diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..bd33fe013 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,17 @@ +{ + "extends": "@antfu", + "rules": { + "no-console": "off", + "no-tabs": "off", + "@typescript-eslint/consistent-type-imports": "off", + "@typescript-eslint/ban-ts-comment": "off" + }, + "ignorePatterns": [ + "node_modules", + "dist", + ".github", + "**.js", + "docs", + "test" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4b6f0072c..ee7917e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,11 @@ coverage apiPassword releasePassword .tmp +npm-debug.log +package-lock.json +lib +dist +**/node_modules +.env .idea +.env diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ae90f7051 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +ignore-workspace-root-check=true diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 96c0ecc38..000000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -docs/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 5e7c57bf1..000000000 --- a/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "tabWidth": 4, - "semi": true, - "singleQuote": false, - "printWidth": 120, - "jsxBracketSameLine": true -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20abbf353..e3cd77462 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ You will need to setup the `supertokens-core` in order to to run the `supertoken ### Prerequisites - OS: Linux or macOS -- Nodejs & npm +- Nodejs & pnpm[installation guide](https://pnpm.js.org/en/installation) - IDE: [VSCode](https://code.visualstudio.com/download)(recommended) or equivalent IDE ### Project Setup @@ -30,15 +30,15 @@ You will need to setup the `supertokens-core` in order to to run the `supertoken `supertokens-node` and `supertokens-root` should exist side by side within the same parent directory 3. `cd supertokens-node` 4. Install the project dependencies - `npm i -d` + `pnpm i` 5. Add git pre-commit hooks - `npm run set-up-hooks` + `pnpm run set-up-hooks` ## Modifying Code 1. Open the `supertokens-node` project in your IDE and you can start modifying the code 2. After modifying the code, build your project to implement your changes - `npm run build-pretty` + `pnpm run build-pretty` ## Testing @@ -48,7 +48,7 @@ You will need to setup the `supertokens-core` in order to to run the `supertoken 3. Navigate to the `supertokens-node` repository `cd ../supertokens-node/` 4. Run all tests - `INSTALL_PATH=../supertokens-root npm test` + `INSTALL_PATH=../supertokens-root pnpm test` 5. If all tests pass the output should be: ![node tests passing](https://github.com/supertokens/supertokens-logo/blob/master/images/supertokens-node-tests-passing.png) diff --git a/framework/awsLambda/index.d.ts b/framework/awsLambda/index.d.ts deleted file mode 100644 index 1b9b0cf57..000000000 --- a/framework/awsLambda/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../lib/build/framework/awsLambda"; -import * as _default from "../../lib/build/framework/awsLambda"; -export default _default; diff --git a/framework/awsLambda/index.js b/framework/awsLambda/index.js deleted file mode 100644 index 071437185..000000000 --- a/framework/awsLambda/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/framework/awsLambda")); diff --git a/framework/express/index.d.ts b/framework/express/index.d.ts deleted file mode 100644 index e8f390773..000000000 --- a/framework/express/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../lib/build/framework/express"; -import * as _default from "../../lib/build/framework/express"; -export default _default; diff --git a/framework/express/index.js b/framework/express/index.js deleted file mode 100644 index 895950024..000000000 --- a/framework/express/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/framework/express")); diff --git a/framework/fastify/index.d.ts b/framework/fastify/index.d.ts deleted file mode 100644 index c5449d9d7..000000000 --- a/framework/fastify/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../lib/build/framework/fastify"; -import * as _default from "../../lib/build/framework/fastify"; -export default _default; diff --git a/framework/fastify/index.js b/framework/fastify/index.js deleted file mode 100644 index b4492ecb8..000000000 --- a/framework/fastify/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/framework/fastify")); diff --git a/framework/hapi/index.d.ts b/framework/hapi/index.d.ts deleted file mode 100644 index 6afcc32c8..000000000 --- a/framework/hapi/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../lib/build/framework/hapi"; -import * as _default from "../../lib/build/framework/hapi"; -export default _default; diff --git a/framework/hapi/index.js b/framework/hapi/index.js deleted file mode 100644 index 598426958..000000000 --- a/framework/hapi/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/framework/hapi")); diff --git a/framework/koa/index.d.ts b/framework/koa/index.d.ts deleted file mode 100644 index d0a190b6d..000000000 --- a/framework/koa/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../lib/build/framework/koa"; -import * as _default from "../../lib/build/framework/koa"; -export default _default; diff --git a/framework/koa/index.js b/framework/koa/index.js deleted file mode 100644 index cd4ad68b9..000000000 --- a/framework/koa/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/framework/koa")); diff --git a/framework/loopback/index.d.ts b/framework/loopback/index.d.ts deleted file mode 100644 index e87459ac3..000000000 --- a/framework/loopback/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../lib/build/framework/loopback"; -import * as _default from "../../lib/build/framework/loopback"; -export default _default; diff --git a/framework/loopback/index.js b/framework/loopback/index.js deleted file mode 100644 index 4c8ef891c..000000000 --- a/framework/loopback/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/framework/loopback")); diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 357f08674..000000000 --- a/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "./lib/build"; -/** - * 'export *' does not re-export a default. - * import SuperTokens from "supertokens-node"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "./lib/build"; -export default _default; diff --git a/index.js b/index.js deleted file mode 100644 index 4e99d89b1..000000000 --- a/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("./lib/build")); diff --git a/lib/build/constants.d.ts b/lib/build/constants.d.ts deleted file mode 100644 index 112aa652a..000000000 --- a/lib/build/constants.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -export declare const HEADER_RID = "rid"; -export declare const HEADER_FDI = "fdi-version"; diff --git a/lib/build/constants.js b/lib/build/constants.js deleted file mode 100644 index ad8a0d0a6..000000000 --- a/lib/build/constants.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.HEADER_FDI = exports.HEADER_RID = void 0; -exports.HEADER_RID = "rid"; -exports.HEADER_FDI = "fdi-version"; diff --git a/lib/build/error.d.ts b/lib/build/error.d.ts deleted file mode 100644 index 41083aef0..000000000 --- a/lib/build/error.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -export default class SuperTokensError extends Error { - private static errMagic; - static BAD_INPUT_ERROR: "BAD_INPUT_ERROR"; - type: string; - payload: any; - fromRecipe: string | undefined; - private errMagic; - constructor( - options: - | { - message: string; - payload?: any; - type: string; - } - | { - message: string; - type: "BAD_INPUT_ERROR"; - payload: undefined; - } - ); - static isErrorFromSuperTokens(obj: any): obj is SuperTokensError; -} diff --git a/lib/build/error.js b/lib/build/error.js deleted file mode 100644 index ad22000d0..000000000 --- a/lib/build/error.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -class SuperTokensError extends Error { - constructor(options) { - super(options.message); - this.type = options.type; - this.payload = options.payload; - this.errMagic = SuperTokensError.errMagic; - } - static isErrorFromSuperTokens(obj) { - return obj.errMagic === SuperTokensError.errMagic; - } -} -exports.default = SuperTokensError; -SuperTokensError.errMagic = "ndskajfasndlfkj435234krjdsa"; -SuperTokensError.BAD_INPUT_ERROR = "BAD_INPUT_ERROR"; diff --git a/lib/build/framework/awsLambda/framework.d.ts b/lib/build/framework/awsLambda/framework.d.ts deleted file mode 100644 index 44d3f7e39..000000000 --- a/lib/build/framework/awsLambda/framework.d.ts +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-nocheck -import type { - APIGatewayProxyEventV2, - APIGatewayProxyEvent, - APIGatewayProxyResult, - APIGatewayProxyStructuredResultV2, - Handler, -} from "aws-lambda"; -import { HTTPMethod } from "../../types"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { SessionContainerInterface } from "../../recipe/session/types"; -import { Framework } from "../types"; -export declare class AWSRequest extends BaseRequest { - private event; - private parsedJSONBody; - private parsedUrlEncodedFormData; - constructor(event: APIGatewayProxyEventV2 | APIGatewayProxyEvent); - getFormData: () => Promise; - getKeyValueFromQuery: (key: string) => string | undefined; - getJSONBody: () => Promise; - getMethod: () => HTTPMethod; - getCookieValue: (key: string) => string | undefined; - getHeaderValue: (key: string) => string | undefined; - getOriginalURL: () => string; -} -interface SupertokensLambdaEvent extends APIGatewayProxyEvent { - supertokens: { - response: { - headers: { - key: string; - value: boolean | number | string; - allowDuplicateKey: boolean; - }[]; - cookies: string[]; - }; - }; -} -interface SupertokensLambdaEventV2 extends APIGatewayProxyEventV2 { - supertokens: { - response: { - headers: { - key: string; - value: boolean | number | string; - allowDuplicateKey: boolean; - }[]; - cookies: string[]; - }; - }; -} -export declare class AWSResponse extends BaseResponse { - private statusCode; - private event; - private content; - responseSet: boolean; - statusSet: boolean; - constructor(event: SupertokensLambdaEvent | SupertokensLambdaEventV2); - sendHTMLResponse: (html: string) => void; - setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - removeHeader: (key: string) => void; - setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - /** - * @param {number} statusCode - */ - setStatusCode: (statusCode: number) => void; - sendJSONResponse: (content: any) => void; - sendResponse: ( - response?: APIGatewayProxyResult | APIGatewayProxyStructuredResultV2 | undefined - ) => APIGatewayProxyResult | APIGatewayProxyStructuredResultV2; -} -export interface SessionEventV2 extends SupertokensLambdaEventV2 { - session?: SessionContainerInterface; -} -export interface SessionEvent extends SupertokensLambdaEvent { - session?: SessionContainerInterface; -} -export declare const middleware: (handler?: Handler | undefined) => Handler; -export interface AWSFramework extends Framework { - middleware: (handler?: Handler) => Handler; -} -export declare const AWSWrapper: AWSFramework; -export {}; diff --git a/lib/build/framework/awsLambda/framework.js b/lib/build/framework/awsLambda/framework.js deleted file mode 100644 index 36c3dc70e..000000000 --- a/lib/build/framework/awsLambda/framework.js +++ /dev/null @@ -1,318 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AWSWrapper = exports.middleware = exports.AWSResponse = exports.AWSRequest = void 0; -const utils_1 = require("../../utils"); -const request_1 = require("../request"); -const response_1 = require("../response"); -const utils_2 = require("../utils"); -const constants_1 = require("../constants"); -const supertokens_1 = __importDefault(require("../../supertokens")); -const querystring_1 = require("querystring"); -const url_1 = require("url"); -class AWSRequest extends request_1.BaseRequest { - constructor(event) { - super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedUrlEncodedFormData === undefined) { - if (this.event.body === null || this.event.body === undefined) { - this.parsedUrlEncodedFormData = {}; - } else { - this.parsedUrlEncodedFormData = querystring_1.parse(this.event.body); - if (this.parsedUrlEncodedFormData === undefined) { - this.parsedUrlEncodedFormData = {}; - } - } - } - return this.parsedUrlEncodedFormData; - }); - this.getKeyValueFromQuery = (key) => { - if (this.event.queryStringParameters === undefined || this.event.queryStringParameters === null) { - return undefined; - } - let value = this.event.queryStringParameters[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedJSONBody === undefined) { - if (this.event.body === null || this.event.body === undefined) { - this.parsedJSONBody = {}; - } else { - this.parsedJSONBody = JSON.parse(this.event.body); - if (this.parsedJSONBody === undefined) { - this.parsedJSONBody = {}; - } - } - } - return this.parsedJSONBody; - }); - this.getMethod = () => { - let rawMethod = this.event.httpMethod; - if (rawMethod !== undefined) { - return utils_1.normaliseHttpMethod(rawMethod); - } - return utils_1.normaliseHttpMethod(this.event.requestContext.http.method); - }; - this.getCookieValue = (key) => { - let cookies = this.event.cookies; - if ( - (this.event.headers === undefined || this.event.headers === null) && - (cookies === undefined || cookies === null) - ) { - return undefined; - } - let value = utils_2.getCookieValueFromHeaders(this.event.headers, key); - if (value === undefined && cookies !== undefined && cookies !== null) { - value = utils_2.getCookieValueFromHeaders( - { - cookie: cookies.join(";"), - }, - key - ); - } - return value; - }; - this.getHeaderValue = (key) => { - if (this.event.headers === undefined || this.event.headers === null) { - return undefined; - } - return utils_2.normalizeHeaderValue(utils_1.getFromObjectCaseInsensitive(key, this.event.headers)); - }; - this.getOriginalURL = () => { - let path = this.event.path; - let queryParams = this.event.queryStringParameters; - if (path === undefined) { - path = this.event.requestContext.http.path; - let stage = this.event.requestContext.stage; - if (stage !== undefined && path.startsWith(`/${stage}`)) { - path = path.slice(stage.length + 1); - } - if (queryParams !== undefined && queryParams !== null) { - let urlString = "https://exmaple.com" + path; - let url = new url_1.URL(urlString); - Object.keys(queryParams).forEach((el) => url.searchParams.append(el, queryParams[el])); - path = url.pathname + url.search; - } - } - return path; - }; - this.original = event; - this.event = event; - this.parsedJSONBody = undefined; - this.parsedUrlEncodedFormData = undefined; - } -} -exports.AWSRequest = AWSRequest; -class AWSResponse extends response_1.BaseResponse { - constructor(event) { - super(); - this.sendHTMLResponse = (html) => { - if (!this.responseSet) { - this.content = html; - this.setHeader("Content-Type", "text/html", false); - this.responseSet = true; - } - }; - this.setHeader = (key, value, allowDuplicateKey) => { - this.event.supertokens.response.headers.push({ - key, - value, - allowDuplicateKey, - }); - }; - this.removeHeader = (key) => { - this.event.supertokens.response.headers = this.event.supertokens.response.headers.filter( - (header) => header.key.toLowerCase() !== key.toLowerCase() - ); - }; - this.setCookie = (key, value, domain, secure, httpOnly, expires, path, sameSite) => { - let serialisedCookie = utils_2.serializeCookieValue( - key, - value, - domain, - secure, - httpOnly, - expires, - path, - sameSite - ); - this.event.supertokens.response.cookies.push(serialisedCookie); - }; - /** - * @param {number} statusCode - */ - this.setStatusCode = (statusCode) => { - if (!this.statusSet) { - this.statusCode = statusCode; - this.statusSet = true; - } - }; - this.sendJSONResponse = (content) => { - if (!this.responseSet) { - this.content = JSON.stringify(content); - this.setHeader("Content-Type", "application/json", false); - this.responseSet = true; - } - }; - this.sendResponse = (response) => { - if (response === undefined) { - response = {}; - } - let headers = response.headers; - if (headers === undefined) { - headers = {}; - } - let body = response.body; - let statusCode = response.statusCode; - if (this.responseSet) { - statusCode = this.statusCode; - body = this.content; - } - let supertokensHeaders = this.event.supertokens.response.headers; - let supertokensCookies = this.event.supertokens.response.cookies; - for (let i = 0; i < supertokensHeaders.length; i++) { - let currentValue = undefined; - let currentHeadersSet = Object.keys(headers === undefined ? [] : headers); - for (let j = 0; j < currentHeadersSet.length; j++) { - if (currentHeadersSet[j].toLowerCase() === supertokensHeaders[i].key.toLowerCase()) { - supertokensHeaders[i].key = currentHeadersSet[j]; - currentValue = headers[currentHeadersSet[j]]; - break; - } - } - if (supertokensHeaders[i].allowDuplicateKey && currentValue !== undefined) { - let newValue = `${currentValue}, ${supertokensHeaders[i].value}`; - headers[supertokensHeaders[i].key] = newValue; - } else { - headers[supertokensHeaders[i].key] = supertokensHeaders[i].value; - } - } - if (this.event.version !== undefined) { - let cookies = response.cookies; - if (cookies === undefined) { - cookies = []; - } - cookies.push(...supertokensCookies); - let result = Object.assign(Object.assign({}, response), { cookies, body, statusCode, headers }); - return result; - } else { - let multiValueHeaders = response.multiValueHeaders; - if (multiValueHeaders === undefined) { - multiValueHeaders = {}; - } - let headsersInMultiValueHeaders = Object.keys(multiValueHeaders); - let cookieHeader = headsersInMultiValueHeaders.find( - (h) => h.toLowerCase() === constants_1.COOKIE_HEADER.toLowerCase() - ); - if (cookieHeader === undefined) { - multiValueHeaders[constants_1.COOKIE_HEADER] = supertokensCookies; - } else { - multiValueHeaders[cookieHeader].push(...supertokensCookies); - } - let result = Object.assign(Object.assign({}, response), { - multiValueHeaders, - body: body, - statusCode: statusCode, - headers, - }); - return result; - } - }; - this.original = event; - this.event = event; - this.statusCode = 200; - this.content = JSON.stringify({}); - this.responseSet = false; - this.statusSet = false; - this.event.supertokens = { - response: { - headers: [], - cookies: [], - }, - }; - } -} -exports.AWSResponse = AWSResponse; -const middleware = (handler) => { - return (event, context, callback) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new AWSRequest(event); - let response = new AWSResponse(event); - try { - let result = yield supertokens.middleware(request, response); - if (result) { - return response.sendResponse(); - } - if (handler !== undefined) { - let handlerResult = yield handler(event, context, callback); - return response.sendResponse(handlerResult); - } - /** - * it reaches this point only if the API route was not exposed by - * the SDK and user didn't provide a handler - */ - response.setStatusCode(404); - response.sendJSONResponse({ - error: `The middleware couldn't serve the API path ${request.getOriginalURL()}, method: ${request.getMethod()}. If this is an unexpected behaviour, please create an issue here: https://github.com/supertokens/supertokens-node/issues`, - }); - return response.sendResponse(); - } catch (err) { - yield supertokens.errorHandler(err, request, response); - if (response.responseSet) { - return response.sendResponse(); - } - throw err; - } - }); -}; -exports.middleware = middleware; -exports.AWSWrapper = { - middleware: exports.middleware, - wrapRequest: (unwrapped) => { - return new AWSRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new AWSResponse(unwrapped); - }, -}; diff --git a/lib/build/framework/awsLambda/index.d.ts b/lib/build/framework/awsLambda/index.d.ts deleted file mode 100644 index 1028174f0..000000000 --- a/lib/build/framework/awsLambda/index.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -export type { SessionEvent, SessionEventV2 } from "./framework"; -export declare const middleware: ( - handler?: import("aws-lambda").Handler | undefined -) => import("aws-lambda").Handler; -export declare const wrapRequest: (unwrapped: any) => import("..").BaseRequest; -export declare const wrapResponse: (unwrapped: any) => import("..").BaseResponse; diff --git a/lib/build/framework/awsLambda/index.js b/lib/build/framework/awsLambda/index.js deleted file mode 100644 index 9e86669f5..000000000 --- a/lib/build/framework/awsLambda/index.js +++ /dev/null @@ -1,21 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapResponse = exports.wrapRequest = exports.middleware = void 0; -const framework_1 = require("./framework"); -exports.middleware = framework_1.AWSWrapper.middleware; -exports.wrapRequest = framework_1.AWSWrapper.wrapRequest; -exports.wrapResponse = framework_1.AWSWrapper.wrapResponse; diff --git a/lib/build/framework/constants.d.ts b/lib/build/framework/constants.d.ts deleted file mode 100644 index eb751247f..000000000 --- a/lib/build/framework/constants.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-nocheck -export declare const COOKIE_HEADER = "Set-Cookie"; diff --git a/lib/build/framework/constants.js b/lib/build/framework/constants.js deleted file mode 100644 index 8a7a8ea6c..000000000 --- a/lib/build/framework/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.COOKIE_HEADER = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -exports.COOKIE_HEADER = "Set-Cookie"; diff --git a/lib/build/framework/express/framework.d.ts b/lib/build/framework/express/framework.d.ts deleted file mode 100644 index 864a3cc92..000000000 --- a/lib/build/framework/express/framework.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -import type { Request, Response, NextFunction } from "express"; -import type { HTTPMethod } from "../../types"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import type { Framework } from "../types"; -import type { SessionContainerInterface } from "../../recipe/session/types"; -export declare class ExpressRequest extends BaseRequest { - private request; - private parserChecked; - private formDataParserChecked; - constructor(request: Request); - getFormData: () => Promise; - getKeyValueFromQuery: (key: string) => string | undefined; - getJSONBody: () => Promise; - getMethod: () => HTTPMethod; - getCookieValue: (key: string) => string | undefined; - getHeaderValue: (key: string) => string | undefined; - getOriginalURL: () => string; -} -export declare class ExpressResponse extends BaseResponse { - private response; - private statusCode; - constructor(response: Response); - sendHTMLResponse: (html: string) => void; - setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - removeHeader: (key: string) => void; - setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - /** - * @param {number} statusCode - */ - setStatusCode: (statusCode: number) => void; - sendJSONResponse: (content: any) => void; -} -export interface SessionRequest extends Request { - session?: SessionContainerInterface; -} -export declare const middleware: () => (req: Request, res: Response, next: NextFunction) => Promise; -export declare const errorHandler: () => (err: any, req: Request, res: Response, next: NextFunction) => Promise; -export interface ExpressFramework extends Framework { - middleware: () => (req: Request, res: Response, next: NextFunction) => Promise; - errorHandler: () => (err: any, req: Request, res: Response, next: NextFunction) => Promise; -} -export declare const ExpressWrapper: ExpressFramework; diff --git a/lib/build/framework/express/framework.js b/lib/build/framework/express/framework.js deleted file mode 100644 index b7201a722..000000000 --- a/lib/build/framework/express/framework.js +++ /dev/null @@ -1,210 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ExpressWrapper = exports.errorHandler = exports.middleware = exports.ExpressResponse = exports.ExpressRequest = void 0; -const utils_1 = require("../../utils"); -const request_1 = require("../request"); -const response_1 = require("../response"); -const utils_2 = require("../utils"); -const supertokens_1 = __importDefault(require("../../supertokens")); -class ExpressRequest extends request_1.BaseRequest { - constructor(request) { - super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.formDataParserChecked) { - yield utils_2.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); - this.formDataParserChecked = true; - } - return this.request.body; - }); - this.getKeyValueFromQuery = (key) => { - if (this.request.query === undefined) { - return undefined; - } - let value = this.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.parserChecked) { - yield utils_2.assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); - this.parserChecked = true; - } - return this.request.body; - }); - this.getMethod = () => { - return utils_1.normaliseHttpMethod(this.request.method); - }; - this.getCookieValue = (key) => { - return utils_2.getCookieValueFromIncomingMessage(this.request, key); - }; - this.getHeaderValue = (key) => { - return utils_2.getHeaderValueFromIncomingMessage(this.request, key); - }; - this.getOriginalURL = () => { - return this.request.originalUrl || this.request.url; - }; - this.original = request; - this.request = request; - this.parserChecked = false; - this.formDataParserChecked = false; - } -} -exports.ExpressRequest = ExpressRequest; -class ExpressResponse extends response_1.BaseResponse { - constructor(response) { - super(); - this.sendHTMLResponse = (html) => { - if (!this.response.writableEnded) { - /** - * response.set method is not available if response - * is a nextjs response object. setHeader method - * is present on OutgoingMessage which is one of the - * bases used to construct response object for express - * like response as well as nextjs like response - */ - this.response.setHeader("Content-Type", "text/html"); - this.response.status(this.statusCode).send(Buffer.from(html)); - } - }; - this.setHeader = (key, value, allowDuplicateKey) => { - utils_2.setHeaderForExpressLikeResponse(this.response, key, value, allowDuplicateKey); - }; - this.removeHeader = (key) => { - this.response.removeHeader(key); - }; - this.setCookie = (key, value, domain, secure, httpOnly, expires, path, sameSite) => { - utils_2.setCookieForServerResponse( - this.response, - key, - value, - domain, - secure, - httpOnly, - expires, - path, - sameSite - ); - }; - /** - * @param {number} statusCode - */ - this.setStatusCode = (statusCode) => { - if (!this.response.writableEnded) { - this.statusCode = statusCode; - } - }; - this.sendJSONResponse = (content) => { - if (!this.response.writableEnded) { - this.response.status(this.statusCode).json(content); - } - }; - this.original = response; - this.response = response; - this.statusCode = 200; - } -} -exports.ExpressResponse = ExpressResponse; -const middleware = () => { - return (req, res, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens; - const request = new ExpressRequest(req); - const response = new ExpressResponse(res); - try { - supertokens = supertokens_1.default.getInstanceOrThrowError(); - const result = yield supertokens.middleware(request, response); - if (!result) { - return next(); - } - } catch (err) { - if (supertokens) { - try { - yield supertokens.errorHandler(err, request, response); - } catch (_a) { - next(err); - } - } else { - next(err); - } - } - }); -}; -exports.middleware = middleware; -const errorHandler = () => { - return (err, req, res, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new ExpressRequest(req); - let response = new ExpressResponse(res); - try { - yield supertokens.errorHandler(err, request, response); - } catch (err) { - return next(err); - } - }); -}; -exports.errorHandler = errorHandler; -exports.ExpressWrapper = { - middleware: exports.middleware, - errorHandler: exports.errorHandler, - wrapRequest: (unwrapped) => { - return new ExpressRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new ExpressResponse(unwrapped); - }, -}; diff --git a/lib/build/framework/express/index.d.ts b/lib/build/framework/express/index.d.ts deleted file mode 100644 index 9bf873aa2..000000000 --- a/lib/build/framework/express/index.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -export type { SessionRequest } from "./framework"; -export declare const middleware: () => ( - req: import("express").Request, - res: import("express").Response, - next: import("express").NextFunction -) => Promise; -export declare const errorHandler: () => ( - err: any, - req: import("express").Request, - res: import("express").Response, - next: import("express").NextFunction -) => Promise; -export declare const wrapRequest: (unwrapped: any) => import("..").BaseRequest; -export declare const wrapResponse: (unwrapped: any) => import("..").BaseResponse; diff --git a/lib/build/framework/express/index.js b/lib/build/framework/express/index.js deleted file mode 100644 index 238165935..000000000 --- a/lib/build/framework/express/index.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapResponse = exports.wrapRequest = exports.errorHandler = exports.middleware = void 0; -const framework_1 = require("./framework"); -exports.middleware = framework_1.ExpressWrapper.middleware; -exports.errorHandler = framework_1.ExpressWrapper.errorHandler; -exports.wrapRequest = framework_1.ExpressWrapper.wrapRequest; -exports.wrapResponse = framework_1.ExpressWrapper.wrapResponse; diff --git a/lib/build/framework/fastify/framework.d.ts b/lib/build/framework/fastify/framework.d.ts deleted file mode 100644 index 0af7536ea..000000000 --- a/lib/build/framework/fastify/framework.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -import type { FastifyRequest as OriginalFastifyRequest, FastifyReply, FastifyPluginCallback } from "fastify"; -import type { HTTPMethod } from "../../types"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import type { Framework } from "../types"; -import type { SessionContainerInterface } from "../../recipe/session/types"; -export declare class FastifyRequest extends BaseRequest { - private request; - constructor(request: OriginalFastifyRequest); - getFormData: () => Promise; - getKeyValueFromQuery: (key: string) => string | undefined; - getJSONBody: () => Promise; - getMethod: () => HTTPMethod; - getCookieValue: (key: string) => string | undefined; - getHeaderValue: (key: string) => string | undefined; - getOriginalURL: () => string; -} -export declare class FastifyResponse extends BaseResponse { - private response; - private statusCode; - constructor(response: FastifyReply); - sendHTMLResponse: (html: string) => void; - setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - removeHeader: (key: string) => void; - setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - /** - * @param {number} statusCode - */ - setStatusCode: (statusCode: number) => void; - /** - * @param {any} content - */ - sendJSONResponse: (content: any) => void; -} -export interface SessionRequest extends OriginalFastifyRequest { - session?: SessionContainerInterface; -} -export interface FasitfyFramework extends Framework { - plugin: FastifyPluginCallback; - errorHandler: () => (err: any, req: OriginalFastifyRequest, res: FastifyReply) => Promise; -} -export declare const errorHandler: () => (err: any, req: OriginalFastifyRequest, res: FastifyReply) => Promise; -export declare const FastifyWrapper: FasitfyFramework; diff --git a/lib/build/framework/fastify/framework.js b/lib/build/framework/fastify/framework.js deleted file mode 100644 index 8c375f9ea..000000000 --- a/lib/build/framework/fastify/framework.js +++ /dev/null @@ -1,234 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.FastifyWrapper = exports.errorHandler = exports.FastifyResponse = exports.FastifyRequest = void 0; -const utils_1 = require("../../utils"); -const request_1 = require("../request"); -const response_1 = require("../response"); -const utils_2 = require("../utils"); -const supertokens_1 = __importDefault(require("../../supertokens")); -const constants_1 = require("../constants"); -class FastifyRequest extends request_1.BaseRequest { - constructor(request) { - super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.body; // NOTE: ask user to add require('fastify-formbody') - }); - this.getKeyValueFromQuery = (key) => { - if (this.request.query === undefined) { - return undefined; - } - let value = this.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.body; - }); - this.getMethod = () => { - return utils_1.normaliseHttpMethod(this.request.method); - }; - this.getCookieValue = (key) => { - return utils_2.getCookieValueFromHeaders(this.request.headers, key); - }; - this.getHeaderValue = (key) => { - return utils_2.normalizeHeaderValue(utils_1.getFromObjectCaseInsensitive(key, this.request.headers)); - }; - this.getOriginalURL = () => { - return this.request.url; - }; - this.original = request; - this.request = request; - } -} -exports.FastifyRequest = FastifyRequest; -class FastifyResponse extends response_1.BaseResponse { - constructor(response) { - super(); - this.sendHTMLResponse = (html) => { - if (!this.response.sent) { - this.response.type("text/html"); - this.response.send(html); - } - }; - this.setHeader = (key, value, allowDuplicateKey) => { - try { - let existingHeaders = this.response.getHeaders(); - let existingValue = existingHeaders[key.toLowerCase()]; - // we have the this.response.header for compatibility with nextJS - if (existingValue === undefined) { - this.response.header(key, value); - } else if (allowDuplicateKey) { - this.response.header(key, existingValue + ", " + value); - } else { - // we overwrite the current one with the new one - this.response.header(key, value); - } - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } - }; - this.removeHeader = (key) => { - this.response.removeHeader(key); - }; - this.setCookie = (key, value, domain, secure, httpOnly, expires, path, sameSite) => { - let serialisedCookie = utils_2.serializeCookieValue( - key, - value, - domain, - secure, - httpOnly, - expires, - path, - sameSite - ); - /** - * lets say if current value is undefined, prev -> undefined - * - * now if add AT, - * cookieValueToSetInHeader -> AT - * response header object will be: - * - * 'set-cookie': AT - * - * now if add RT, - * - * prev -> AT - * cookieValueToSetInHeader -> AT + RT - * and response header object will be: - * - * 'set-cookie': AT + AT + RT - * - * now if add IRT, - * - * prev -> AT + AT + RT - * cookieValueToSetInHeader -> IRT + AT + AT + RT - * and response header object will be: - * - * 'set-cookie': AT + AT + RT + IRT + AT + AT + RT - * - * To avoid this, we no longer get and use the previous value - * - * Old code: - * - * let prev: string | string[] | undefined = this.response.getHeader(COOKIE_HEADER) as - * | string - * | string[] - * | undefined; - * let cookieValueToSetInHeader = getCookieValueToSetInHeader(prev, serialisedCookie, key); - * this.response.header(COOKIE_HEADER, cookieValueToSetInHeader); - */ - this.response.header(constants_1.COOKIE_HEADER, serialisedCookie); - }; - /** - * @param {number} statusCode - */ - this.setStatusCode = (statusCode) => { - if (!this.response.sent) { - this.statusCode = statusCode; - } - }; - /** - * @param {any} content - */ - this.sendJSONResponse = (content) => { - if (!this.response.sent) { - this.response.statusCode = this.statusCode; - this.response.send(content); - } - }; - this.original = response; - this.response = response; - this.statusCode = 200; - } -} -exports.FastifyResponse = FastifyResponse; -function plugin(fastify, _, done) { - fastify.addHook("preHandler", (req, reply) => - __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new FastifyRequest(req); - let response = new FastifyResponse(reply); - try { - yield supertokens.middleware(request, response); - } catch (err) { - yield supertokens.errorHandler(err, request, response); - } - }) - ); - done(); -} -plugin[Symbol.for("skip-override")] = true; -const errorHandler = () => { - return (err, req, res) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new FastifyRequest(req); - let response = new FastifyResponse(res); - yield supertokens.errorHandler(err, request, response); - }); -}; -exports.errorHandler = errorHandler; -exports.FastifyWrapper = { - plugin, - errorHandler: exports.errorHandler, - wrapRequest: (unwrapped) => { - return new FastifyRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new FastifyResponse(unwrapped); - }, -}; diff --git a/lib/build/framework/fastify/index.d.ts b/lib/build/framework/fastify/index.d.ts deleted file mode 100644 index 3cca3a1a2..000000000 --- a/lib/build/framework/fastify/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -/// -export type { SessionRequest } from "./framework"; -export declare const plugin: import("fastify").FastifyPluginCallback, import("http").Server>; -export declare const errorHandler: () => ( - err: any, - req: import("fastify").FastifyRequest< - import("fastify/types/route").RouteGenericInterface, - import("http").Server, - import("http").IncomingMessage - >, - res: import("fastify").FastifyReply< - import("http").Server, - import("http").IncomingMessage, - import("http").ServerResponse, - import("fastify/types/route").RouteGenericInterface, - unknown - > -) => Promise; -export declare const wrapRequest: (unwrapped: any) => import("..").BaseRequest; -export declare const wrapResponse: (unwrapped: any) => import("..").BaseResponse; diff --git a/lib/build/framework/fastify/index.js b/lib/build/framework/fastify/index.js deleted file mode 100644 index 3b855ed7e..000000000 --- a/lib/build/framework/fastify/index.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapResponse = exports.wrapRequest = exports.errorHandler = exports.plugin = void 0; -const framework_1 = require("./framework"); -exports.plugin = framework_1.FastifyWrapper.plugin; -exports.errorHandler = framework_1.FastifyWrapper.errorHandler; -exports.wrapRequest = framework_1.FastifyWrapper.wrapRequest; -exports.wrapResponse = framework_1.FastifyWrapper.wrapResponse; diff --git a/lib/build/framework/hapi/framework.d.ts b/lib/build/framework/hapi/framework.d.ts deleted file mode 100644 index 5771d86e8..000000000 --- a/lib/build/framework/hapi/framework.d.ts +++ /dev/null @@ -1,63 +0,0 @@ -// @ts-nocheck -import type { Request, ResponseToolkit, Plugin, ResponseObject } from "@hapi/hapi"; -import type { HTTPMethod } from "../../types"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import type { Framework } from "../types"; -import type { SessionContainerInterface } from "../../recipe/session/types"; -export declare class HapiRequest extends BaseRequest { - private request; - constructor(request: Request); - getFormData: () => Promise; - getKeyValueFromQuery: (key: string) => string | undefined; - getJSONBody: () => Promise; - getMethod: () => HTTPMethod; - getCookieValue: (key: string) => string | undefined; - getHeaderValue: (key: string) => string | undefined; - getOriginalURL: () => string; -} -export interface ExtendedResponseToolkit extends ResponseToolkit { - lazyHeaderBindings: ( - h: ResponseToolkit, - key: string, - value: string | undefined, - allowDuplicateKey: boolean - ) => void; -} -export declare class HapiResponse extends BaseResponse { - private response; - private statusCode; - private content; - responseSet: boolean; - statusSet: boolean; - constructor(response: ExtendedResponseToolkit); - sendHTMLResponse: (html: string) => void; - setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - removeHeader: (key: string) => void; - setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - /** - * @param {number} statusCode - */ - setStatusCode: (statusCode: number) => void; - /** - * @param {any} content - */ - sendJSONResponse: (content: any) => void; - sendResponse: (overwriteHeaders?: boolean) => ResponseObject; -} -export interface SessionRequest extends Request { - session?: SessionContainerInterface; -} -export interface HapiFramework extends Framework { - plugin: Plugin<{}>; -} -export declare const HapiWrapper: HapiFramework; diff --git a/lib/build/framework/hapi/framework.js b/lib/build/framework/hapi/framework.js deleted file mode 100644 index 08cd1cff8..000000000 --- a/lib/build/framework/hapi/framework.js +++ /dev/null @@ -1,259 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.HapiWrapper = exports.HapiResponse = exports.HapiRequest = void 0; -const utils_1 = require("../../utils"); -const request_1 = require("../request"); -const response_1 = require("../response"); -const utils_2 = require("../utils"); -const supertokens_1 = __importDefault(require("../../supertokens")); -class HapiRequest extends request_1.BaseRequest { - constructor(request) { - super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; - }); - this.getKeyValueFromQuery = (key) => { - if (this.request.query === undefined) { - return undefined; - } - let value = this.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; - }); - this.getMethod = () => { - return utils_1.normaliseHttpMethod(this.request.method); - }; - this.getCookieValue = (key) => { - return utils_2.getCookieValueFromHeaders(this.request.headers, key); - }; - this.getHeaderValue = (key) => { - return utils_2.normalizeHeaderValue(this.request.headers[key]); - }; - this.getOriginalURL = () => { - return this.request.url.toString(); - }; - this.original = request; - this.request = request; - } -} -exports.HapiRequest = HapiRequest; -class HapiResponse extends response_1.BaseResponse { - constructor(response) { - super(); - this.statusSet = false; - this.sendHTMLResponse = (html) => { - if (!this.responseSet) { - this.content = html; - this.setHeader("Content-Type", "text/html", false); - this.responseSet = true; - } - }; - this.setHeader = (key, value, allowDuplicateKey) => { - try { - this.response.lazyHeaderBindings(this.response, key, value, allowDuplicateKey); - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } - }; - this.removeHeader = (key) => { - this.response.lazyHeaderBindings(this.response, key, undefined, false); - }; - this.setCookie = (key, value, domain, secure, httpOnly, expires, path, sameSite) => { - let now = Date.now(); - if (expires > now) { - this.response.state(key, value, { - isHttpOnly: httpOnly, - isSecure: secure, - path: path, - domain, - ttl: expires - now, - isSameSite: sameSite === "lax" ? "Lax" : sameSite === "none" ? "None" : "Strict", - }); - } else { - this.response.unstate(key); - } - }; - /** - * @param {number} statusCode - */ - this.setStatusCode = (statusCode) => { - if (!this.statusSet) { - this.statusCode = statusCode; - this.statusSet = true; - } - }; - /** - * @param {any} content - */ - this.sendJSONResponse = (content) => { - if (!this.responseSet) { - this.content = content; - this.responseSet = true; - } - }; - this.sendResponse = (overwriteHeaders = false) => { - if (!overwriteHeaders) { - return this.response.response(this.content).code(this.statusCode).takeover(); - } - return this.response.response(this.content).code(this.statusCode); - }; - this.original = response; - this.response = response; - this.statusCode = 200; - this.content = null; - this.responseSet = false; - } -} -exports.HapiResponse = HapiResponse; -const plugin = { - name: "supertokens-hapi-middleware", - version: "1.0.0", - register: function (server, _) { - return __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - server.ext("onPreHandler", (req, h) => - __awaiter(this, void 0, void 0, function* () { - let request = new HapiRequest(req); - let response = new HapiResponse(h); - let result = yield supertokens.middleware(request, response); - if (!result) { - return h.continue; - } - return response.sendResponse(); - }) - ); - server.ext("onPreResponse", (request, h) => - __awaiter(this, void 0, void 0, function* () { - (request.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { - if (request.response.isBoom) { - request.response.output.headers[key] = value; - } else { - request.response.header(key, value, { append: allowDuplicateKey }); - } - }); - if (request.response.isBoom) { - let err = request.response.data || request.response; - let req = new HapiRequest(request); - let res = new HapiResponse(h); - if (err !== undefined && err !== null) { - try { - yield supertokens.errorHandler(err, req, res); - if (res.responseSet) { - let resObj = res.sendResponse(true); - (request.app.lazyHeaders || []).forEach(({ key, value, allowDuplicateKey }) => { - resObj.header(key, value, { append: allowDuplicateKey }); - }); - return resObj.takeover(); - } - return h.continue; - } catch (e) { - return h.continue; - } - } - } - return h.continue; - }) - ); - server.decorate("toolkit", "lazyHeaderBindings", function (h, key, value, allowDuplicateKey) { - const anyApp = h.request.app; - anyApp.lazyHeaders = anyApp.lazyHeaders || []; - if (value === undefined) { - anyApp.lazyHeaders = anyApp.lazyHeaders.filter( - (header) => header.key.toLowerCase() !== key.toLowerCase() - ); - } else { - anyApp.lazyHeaders.push({ key, value, allowDuplicateKey }); - } - }); - let supportedRoutes = []; - let routeMethodSet = new Set(); - for (let i = 0; i < supertokens.recipeModules.length; i++) { - let apisHandled = supertokens.recipeModules[i].getAPIsHandled(); - for (let j = 0; j < apisHandled.length; j++) { - let api = apisHandled[j]; - if (!api.disabled) { - let path = `${supertokens.appInfo.apiBasePath.getAsStringDangerous()}${api.pathWithoutApiBasePath.getAsStringDangerous()}`; - let methodAndPath = `${api.method}-${path}`; - if (!routeMethodSet.has(methodAndPath)) { - supportedRoutes.push({ - path, - method: api.method, - handler: (_, h) => { - return h.continue; - }, - }); - routeMethodSet.add(methodAndPath); - } - } - } - } - server.route(supportedRoutes); - }); - }, -}; -exports.HapiWrapper = { - plugin, - wrapRequest: (unwrapped) => { - return new HapiRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new HapiResponse(unwrapped); - }, -}; diff --git a/lib/build/framework/hapi/index.d.ts b/lib/build/framework/hapi/index.d.ts deleted file mode 100644 index ea293f69b..000000000 --- a/lib/build/framework/hapi/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -export type { SessionRequest } from "./framework"; -export declare const plugin: import("@hapi/hapi").Plugin<{}>; -export declare const wrapRequest: (unwrapped: any) => import("..").BaseRequest; -export declare const wrapResponse: (unwrapped: any) => import("..").BaseResponse; diff --git a/lib/build/framework/hapi/index.js b/lib/build/framework/hapi/index.js deleted file mode 100644 index 5aed04807..000000000 --- a/lib/build/framework/hapi/index.js +++ /dev/null @@ -1,21 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapResponse = exports.wrapRequest = exports.plugin = void 0; -const framework_1 = require("./framework"); -exports.plugin = framework_1.HapiWrapper.plugin; -exports.wrapRequest = framework_1.HapiWrapper.wrapRequest; -exports.wrapResponse = framework_1.HapiWrapper.wrapResponse; diff --git a/lib/build/framework/index.d.ts b/lib/build/framework/index.d.ts deleted file mode 100644 index 8fd8891ce..000000000 --- a/lib/build/framework/index.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -export { BaseRequest } from "./request"; -export { BaseResponse } from "./response"; -import * as expressFramework from "./express"; -import * as fastifyFramework from "./fastify"; -import * as hapiFramework from "./hapi"; -import * as loopbackFramework from "./loopback"; -import * as koaFramework from "./koa"; -import * as awsLambdaFramework from "./awsLambda"; -declare const _default: { - express: typeof expressFramework; - fastify: typeof fastifyFramework; - hapi: typeof hapiFramework; - loopback: typeof loopbackFramework; - koa: typeof koaFramework; - awsLambda: typeof awsLambdaFramework; -}; -export default _default; -export declare let express: typeof expressFramework; -export declare let fastify: typeof fastifyFramework; -export declare let hapi: typeof hapiFramework; -export declare let loopback: typeof loopbackFramework; -export declare let koa: typeof koaFramework; -export declare let awsLambda: typeof awsLambdaFramework; diff --git a/lib/build/framework/index.js b/lib/build/framework/index.js deleted file mode 100644 index 28dc9790d..000000000 --- a/lib/build/framework/index.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.awsLambda = exports.koa = exports.loopback = exports.hapi = exports.fastify = exports.express = exports.BaseResponse = exports.BaseRequest = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var request_1 = require("./request"); -Object.defineProperty(exports, "BaseRequest", { - enumerable: true, - get: function () { - return request_1.BaseRequest; - }, -}); -var response_1 = require("./response"); -Object.defineProperty(exports, "BaseResponse", { - enumerable: true, - get: function () { - return response_1.BaseResponse; - }, -}); -const expressFramework = __importStar(require("./express")); -const fastifyFramework = __importStar(require("./fastify")); -const hapiFramework = __importStar(require("./hapi")); -const loopbackFramework = __importStar(require("./loopback")); -const koaFramework = __importStar(require("./koa")); -const awsLambdaFramework = __importStar(require("./awsLambda")); -exports.default = { - express: expressFramework, - fastify: fastifyFramework, - hapi: hapiFramework, - loopback: loopbackFramework, - koa: koaFramework, - awsLambda: awsLambdaFramework, -}; -exports.express = expressFramework; -exports.fastify = fastifyFramework; -exports.hapi = hapiFramework; -exports.loopback = loopbackFramework; -exports.koa = koaFramework; -exports.awsLambda = awsLambdaFramework; diff --git a/lib/build/framework/koa/framework.d.ts b/lib/build/framework/koa/framework.d.ts deleted file mode 100644 index 96514a08d..000000000 --- a/lib/build/framework/koa/framework.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -// @ts-nocheck -import type { Context, Next } from "koa"; -import type { HTTPMethod } from "../../types"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { SessionContainerInterface } from "../../recipe/session/types"; -import { Framework } from "../types"; -export declare class KoaRequest extends BaseRequest { - private ctx; - private parsedJSONBody; - private parsedUrlEncodedFormData; - constructor(ctx: Context); - getFormData: () => Promise; - getKeyValueFromQuery: (key: string) => string | undefined; - getJSONBody: () => Promise; - getMethod: () => HTTPMethod; - getCookieValue: (key: string) => string | undefined; - getHeaderValue: (key: string) => string | undefined; - getOriginalURL: () => string; -} -export declare class KoaResponse extends BaseResponse { - private ctx; - responseSet: boolean; - statusSet: boolean; - constructor(ctx: Context); - sendHTMLResponse: (html: string) => void; - setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - removeHeader: (key: string) => void; - setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - /** - * @param {number} statusCode - */ - setStatusCode: (statusCode: number) => void; - sendJSONResponse: (content: any) => void; -} -export interface SessionContext extends Context { - session?: SessionContainerInterface; -} -export declare const middleware: () => (ctx: Context, next: Next) => Promise; -export interface KoaFramework extends Framework { - middleware: () => (ctx: Context, next: Next) => Promise; -} -export declare const KoaWrapper: KoaFramework; diff --git a/lib/build/framework/koa/framework.js b/lib/build/framework/koa/framework.js deleted file mode 100644 index ac611bae5..000000000 --- a/lib/build/framework/koa/framework.js +++ /dev/null @@ -1,208 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.KoaWrapper = exports.middleware = exports.KoaResponse = exports.KoaRequest = void 0; -const utils_1 = require("../../utils"); -const request_1 = require("../request"); -const response_1 = require("../response"); -const utils_2 = require("../utils"); -const co_body_1 = require("co-body"); -const supertokens_1 = __importDefault(require("../../supertokens")); -class KoaRequest extends request_1.BaseRequest { - constructor(ctx) { - super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedUrlEncodedFormData === undefined) { - this.parsedUrlEncodedFormData = yield parseURLEncodedFormData(this.ctx); - } - return this.parsedUrlEncodedFormData; - }); - this.getKeyValueFromQuery = (key) => { - if (this.ctx.query === undefined) { - return undefined; - } - let value = this.ctx.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (this.parsedJSONBody === undefined) { - this.parsedJSONBody = yield parseJSONBodyFromRequest(this.ctx); - } - return this.parsedJSONBody === undefined ? {} : this.parsedJSONBody; - }); - this.getMethod = () => { - return utils_1.normaliseHttpMethod(this.ctx.request.method); - }; - this.getCookieValue = (key) => { - return this.ctx.cookies.get(key); - }; - this.getHeaderValue = (key) => { - return utils_2.getHeaderValueFromIncomingMessage(this.ctx.req, key); - }; - this.getOriginalURL = () => { - return this.ctx.originalUrl; - }; - this.original = ctx; - this.ctx = ctx; - this.parsedJSONBody = undefined; - this.parsedUrlEncodedFormData = undefined; - } -} -exports.KoaRequest = KoaRequest; -function parseJSONBodyFromRequest(ctx) { - return __awaiter(this, void 0, void 0, function* () { - if (ctx.body !== undefined) { - return ctx.body; - } - return yield co_body_1.json(ctx); - }); -} -function parseURLEncodedFormData(ctx) { - return __awaiter(this, void 0, void 0, function* () { - if (ctx.body !== undefined) { - return ctx.body; - } - return yield co_body_1.form(ctx); - }); -} -class KoaResponse extends response_1.BaseResponse { - constructor(ctx) { - super(); - this.responseSet = false; - this.statusSet = false; - this.sendHTMLResponse = (html) => { - if (!this.responseSet) { - this.ctx.set("content-type", "text/html"); - this.ctx.body = html; - this.responseSet = true; - } - }; - this.setHeader = (key, value, allowDuplicateKey) => { - try { - let existingHeaders = this.ctx.response.headers; - let existingValue = existingHeaders[key.toLowerCase()]; - if (existingValue === undefined) { - this.ctx.set(key, value); - } else if (allowDuplicateKey) { - this.ctx.set(key, existingValue + ", " + value); - } else { - // we overwrite the current one with the new one - this.ctx.set(key, value); - } - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } - }; - this.removeHeader = (key) => { - this.ctx.remove(key); - }; - this.setCookie = (key, value, domain, secure, httpOnly, expires, path, sameSite) => { - this.ctx.cookies.set(key, value, { - secure, - sameSite, - httpOnly, - expires: new Date(expires), - domain, - path, - }); - }; - /** - * @param {number} statusCode - */ - this.setStatusCode = (statusCode) => { - if (!this.statusSet) { - this.ctx.status = statusCode; - this.statusSet = true; - } - }; - this.sendJSONResponse = (content) => { - if (!this.responseSet) { - this.ctx.body = content; - this.responseSet = true; - } - }; - this.original = ctx; - this.ctx = ctx; - } -} -exports.KoaResponse = KoaResponse; -const middleware = () => { - return (ctx, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new KoaRequest(ctx); - let response = new KoaResponse(ctx); - try { - let result = yield supertokens.middleware(request, response); - if (!result) { - return yield next(); - } - } catch (err) { - return yield supertokens.errorHandler(err, request, response); - } - }); -}; -exports.middleware = middleware; -exports.KoaWrapper = { - middleware: exports.middleware, - wrapRequest: (unwrapped) => { - return new KoaRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new KoaResponse(unwrapped); - }, -}; diff --git a/lib/build/framework/koa/index.d.ts b/lib/build/framework/koa/index.d.ts deleted file mode 100644 index 1d6395964..000000000 --- a/lib/build/framework/koa/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -export type { SessionContext } from "./framework"; -export declare const middleware: () => (ctx: import("koa").Context, next: import("koa").Next) => Promise; -export declare const wrapRequest: (unwrapped: any) => import("..").BaseRequest; -export declare const wrapResponse: (unwrapped: any) => import("..").BaseResponse; diff --git a/lib/build/framework/koa/index.js b/lib/build/framework/koa/index.js deleted file mode 100644 index 4f7150a4f..000000000 --- a/lib/build/framework/koa/index.js +++ /dev/null @@ -1,21 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapResponse = exports.wrapRequest = exports.middleware = void 0; -const framework_1 = require("./framework"); -exports.middleware = framework_1.KoaWrapper.middleware; -exports.wrapRequest = framework_1.KoaWrapper.wrapRequest; -exports.wrapResponse = framework_1.KoaWrapper.wrapResponse; diff --git a/lib/build/framework/loopback/framework.d.ts b/lib/build/framework/loopback/framework.d.ts deleted file mode 100644 index 51907ee8f..000000000 --- a/lib/build/framework/loopback/framework.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -// @ts-nocheck -import type { MiddlewareContext, Response, Middleware } from "@loopback/rest"; -import { SessionContainerInterface } from "../../recipe/session/types"; -import { HTTPMethod } from "../../types"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import type { Framework } from "../types"; -export declare class LoopbackRequest extends BaseRequest { - private request; - private parserChecked; - private formDataParserChecked; - constructor(ctx: MiddlewareContext); - getFormData: () => Promise; - getKeyValueFromQuery: (key: string) => string | undefined; - getJSONBody: () => Promise; - getMethod: () => HTTPMethod; - getCookieValue: (key: string) => string | undefined; - getHeaderValue: (key: string) => string | undefined; - getOriginalURL: () => string; -} -export declare class LoopbackResponse extends BaseResponse { - response: Response; - private statusCode; - constructor(ctx: MiddlewareContext); - sendHTMLResponse: (html: string) => void; - setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - removeHeader: (key: string) => void; - setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - setStatusCode: (statusCode: number) => void; - sendJSONResponse: (content: any) => void; -} -export interface SessionContext extends MiddlewareContext { - session?: SessionContainerInterface; -} -export interface LoopbackFramework extends Framework { - middleware: Middleware; -} -export declare const middleware: Middleware; -export declare const LoopbackWrapper: LoopbackFramework; diff --git a/lib/build/framework/loopback/framework.js b/lib/build/framework/loopback/framework.js deleted file mode 100644 index 7e8db8ee6..000000000 --- a/lib/build/framework/loopback/framework.js +++ /dev/null @@ -1,175 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.LoopbackWrapper = exports.middleware = exports.LoopbackResponse = exports.LoopbackRequest = void 0; -const utils_1 = require("../../utils"); -const request_1 = require("../request"); -const response_1 = require("../response"); -const utils_2 = require("../utils"); -const supertokens_1 = __importDefault(require("../../supertokens")); -class LoopbackRequest extends request_1.BaseRequest { - constructor(ctx) { - super(); - this.getFormData = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.formDataParserChecked) { - yield utils_2.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); - this.formDataParserChecked = true; - } - return this.request.body; - }); - this.getKeyValueFromQuery = (key) => { - if (this.request.query === undefined) { - return undefined; - } - let value = this.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - this.getJSONBody = () => - __awaiter(this, void 0, void 0, function* () { - if (!this.parserChecked) { - yield utils_2.assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); - this.parserChecked = true; - } - return this.request.body; - }); - this.getMethod = () => { - return utils_1.normaliseHttpMethod(this.request.method); - }; - this.getCookieValue = (key) => { - return utils_2.getCookieValueFromIncomingMessage(this.request, key); - }; - this.getHeaderValue = (key) => { - return utils_2.getHeaderValueFromIncomingMessage(this.request, key); - }; - this.getOriginalURL = () => { - return this.request.originalUrl; - }; - this.original = ctx.request; - this.request = ctx.request; - this.parserChecked = false; - this.formDataParserChecked = false; - } -} -exports.LoopbackRequest = LoopbackRequest; -class LoopbackResponse extends response_1.BaseResponse { - constructor(ctx) { - super(); - this.sendHTMLResponse = (html) => { - if (!this.response.writableEnded) { - this.response.set("Content-Type", "text/html"); - this.response.status(this.statusCode).send(Buffer.from(html)); - } - }; - this.setHeader = (key, value, allowDuplicateKey) => { - utils_2.setHeaderForExpressLikeResponse(this.response, key, value, allowDuplicateKey); - }; - this.removeHeader = (key) => { - this.response.removeHeader(key); - }; - this.setCookie = (key, value, domain, secure, httpOnly, expires, path, sameSite) => { - utils_2.setCookieForServerResponse( - this.response, - key, - value, - domain, - secure, - httpOnly, - expires, - path, - sameSite - ); - }; - this.setStatusCode = (statusCode) => { - if (!this.response.writableEnded) { - this.statusCode = statusCode; - } - }; - this.sendJSONResponse = (content) => { - if (!this.response.writableEnded) { - this.response.status(this.statusCode).json(content); - } - }; - this.original = ctx.response; - this.response = ctx.response; - this.statusCode = 200; - } -} -exports.LoopbackResponse = LoopbackResponse; -const middleware = (ctx, next) => - __awaiter(void 0, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new LoopbackRequest(ctx); - let response = new LoopbackResponse(ctx); - try { - let result = yield supertokens.middleware(request, response); - if (!result) { - return yield next(); - } - return; - } catch (err) { - return yield supertokens.errorHandler(err, request, response); - } - }); -exports.middleware = middleware; -exports.LoopbackWrapper = { - middleware: exports.middleware, - wrapRequest: (unwrapped) => { - return new LoopbackRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new LoopbackResponse(unwrapped); - }, -}; diff --git a/lib/build/framework/loopback/index.d.ts b/lib/build/framework/loopback/index.d.ts deleted file mode 100644 index 4074a2fd5..000000000 --- a/lib/build/framework/loopback/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -export type { SessionContext } from "./framework"; -export declare const middleware: import("@loopback/rest").Middleware; -export declare const wrapRequest: (unwrapped: any) => import("..").BaseRequest; -export declare const wrapResponse: (unwrapped: any) => import("..").BaseResponse; diff --git a/lib/build/framework/loopback/index.js b/lib/build/framework/loopback/index.js deleted file mode 100644 index ae5cb6ad4..000000000 --- a/lib/build/framework/loopback/index.js +++ /dev/null @@ -1,21 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.wrapResponse = exports.wrapRequest = exports.middleware = void 0; -const framework_1 = require("./framework"); -exports.middleware = framework_1.LoopbackWrapper.middleware; -exports.wrapRequest = framework_1.LoopbackWrapper.wrapRequest; -exports.wrapResponse = framework_1.LoopbackWrapper.wrapResponse; diff --git a/lib/build/framework/request.d.ts b/lib/build/framework/request.d.ts deleted file mode 100644 index 17513cb35..000000000 --- a/lib/build/framework/request.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -import { HTTPMethod } from "../types"; -export declare abstract class BaseRequest { - wrapperUsed: boolean; - original: any; - constructor(); - abstract getKeyValueFromQuery: (key: string) => string | undefined; - abstract getJSONBody: () => Promise; - abstract getMethod: () => HTTPMethod; - abstract getCookieValue: (key_: string) => string | undefined; - abstract getHeaderValue: (key: string) => string | undefined; - abstract getOriginalURL: () => string; - abstract getFormData: () => Promise; -} diff --git a/lib/build/framework/request.js b/lib/build/framework/request.js deleted file mode 100644 index d3d8773e6..000000000 --- a/lib/build/framework/request.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BaseRequest = void 0; -class BaseRequest { - constructor() { - this.wrapperUsed = true; - } -} -exports.BaseRequest = BaseRequest; diff --git a/lib/build/framework/response.d.ts b/lib/build/framework/response.d.ts deleted file mode 100644 index 8cf7a67d3..000000000 --- a/lib/build/framework/response.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -export declare abstract class BaseResponse { - wrapperUsed: boolean; - original: any; - constructor(); - abstract setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - abstract removeHeader: (key: string) => void; - abstract setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - abstract setStatusCode: (statusCode: number) => void; - abstract sendJSONResponse: (content: any) => void; - abstract sendHTMLResponse: (html: string) => void; -} diff --git a/lib/build/framework/response.js b/lib/build/framework/response.js deleted file mode 100644 index d634020ad..000000000 --- a/lib/build/framework/response.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BaseResponse = void 0; -class BaseResponse { - constructor() { - this.wrapperUsed = true; - } -} -exports.BaseResponse = BaseResponse; diff --git a/lib/build/framework/types.d.ts b/lib/build/framework/types.d.ts deleted file mode 100644 index f685b5e0a..000000000 --- a/lib/build/framework/types.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -export declare type TypeFramework = "express" | "fastify" | "hapi" | "loopback" | "koa" | "awsLambda"; -import { BaseRequest, BaseResponse } from "."; -export declare let SchemaFramework: { - type: string; - enum: string[]; -}; -export interface Framework { - wrapRequest: (unwrapped: any) => BaseRequest; - wrapResponse: (unwrapped: any) => BaseResponse; -} diff --git a/lib/build/framework/types.js b/lib/build/framework/types.js deleted file mode 100644 index 282217eef..000000000 --- a/lib/build/framework/types.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SchemaFramework = void 0; -exports.SchemaFramework = { - type: "string", - enum: ["express", "fastify", "hapi", "loopback", "koa", "awsLambda"], -}; diff --git a/lib/build/framework/utils.d.ts b/lib/build/framework/utils.d.ts deleted file mode 100644 index c9e1a80c1..000000000 --- a/lib/build/framework/utils.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-nocheck -/// -import type { Request, Response } from "express"; -import type { IncomingMessage } from "http"; -import { ServerResponse } from "http"; -import type { HTTPMethod } from "../types"; -import { NextApiRequest } from "next"; -export declare function getCookieValueFromHeaders(headers: any, key: string): string | undefined; -export declare function getCookieValueFromIncomingMessage(request: IncomingMessage, key: string): string | undefined; -export declare function getHeaderValueFromIncomingMessage(request: IncomingMessage, key: string): string | undefined; -export declare function normalizeHeaderValue(value: string | string[] | undefined): string | undefined; -export declare function assertThatBodyParserHasBeenUsedForExpressLikeRequest( - method: HTTPMethod, - request: (Request | NextApiRequest) & { - __supertokensFromNextJS?: true; - } -): Promise; -export declare function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest( - request: (Request | NextApiRequest) & { - __supertokensFromNextJS?: true; - } -): Promise; -export declare function setHeaderForExpressLikeResponse( - res: Response, - key: string, - value: string, - allowDuplicateKey: boolean -): void; -/** - * - * @param res - * @param name - * @param value - * @param domain - * @param secure - * @param httpOnly - * @param expires - * @param path - */ -export declare function setCookieForServerResponse( - res: ServerResponse, - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" -): ServerResponse; -export declare function getCookieValueToSetInHeader( - prev: string | string[] | undefined, - val: string | string[], - key: string -): string | string[]; -export declare function serializeCookieValue( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" -): string; diff --git a/lib/build/framework/utils.js b/lib/build/framework/utils.js deleted file mode 100644 index ee94f30de..000000000 --- a/lib/build/framework/utils.js +++ /dev/null @@ -1,315 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.serializeCookieValue = exports.getCookieValueToSetInHeader = exports.setCookieForServerResponse = exports.setHeaderForExpressLikeResponse = exports.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest = exports.assertThatBodyParserHasBeenUsedForExpressLikeRequest = exports.normalizeHeaderValue = exports.getHeaderValueFromIncomingMessage = exports.getCookieValueFromIncomingMessage = exports.getCookieValueFromHeaders = void 0; -const cookie_1 = require("cookie"); -const body_parser_1 = require("body-parser"); -const http_1 = require("http"); -const error_1 = __importDefault(require("../error")); -const constants_1 = require("./constants"); -const utils_1 = require("../utils"); -function getCookieValueFromHeaders(headers, key) { - if (headers === undefined || headers === null) { - return undefined; - } - let cookies = headers.cookie || headers.Cookie; - if (cookies === undefined) { - return undefined; - } - cookies = cookie_1.parse(cookies); - // parse JSON cookies - cookies = JSONCookies(cookies); - return cookies[key]; -} -exports.getCookieValueFromHeaders = getCookieValueFromHeaders; -function getCookieValueFromIncomingMessage(request, key) { - if (request.cookies) { - return request.cookies[key]; - } - return getCookieValueFromHeaders(request.headers, key); -} -exports.getCookieValueFromIncomingMessage = getCookieValueFromIncomingMessage; -function getHeaderValueFromIncomingMessage(request, key) { - return normalizeHeaderValue(utils_1.getFromObjectCaseInsensitive(key, request.headers)); -} -exports.getHeaderValueFromIncomingMessage = getHeaderValueFromIncomingMessage; -function normalizeHeaderValue(value) { - if (value === undefined) { - return undefined; - } - if (Array.isArray(value)) { - return value[0]; - } - return value; -} -exports.normalizeHeaderValue = normalizeHeaderValue; -/** - * Parse JSON cookie string. - * - * @param {String} str - * @return {Object} Parsed object or undefined if not json cookie - * @public - */ -function JSONCookie(str) { - if (typeof str !== "string" || str.substr(0, 2) !== "j:") { - return undefined; - } - try { - return JSON.parse(str.slice(2)); - } catch (err) { - return undefined; - } -} -/** - * Parse JSON cookies. - * - * @param {Object} obj - * @return {Object} - * @public - */ -function JSONCookies(obj) { - let cookies = Object.keys(obj); - let key; - let val; - for (let i = 0; i < cookies.length; i++) { - key = cookies[i]; - val = JSONCookie(obj[key]); - if (val) { - obj[key] = val; - } - } - return obj; -} -function assertThatBodyParserHasBeenUsedForExpressLikeRequest(method, request) { - return __awaiter(this, void 0, void 0, function* () { - // according to https://github.com/supertokens/supertokens-node/issues/33 - if (method === "post" || method === "put") { - if (typeof request.body === "string") { - try { - request.body = JSON.parse(request.body); - } catch (err) { - if (request.body === "") { - request.body = {}; - } else { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass a valid JSON input in the request body", - }); - } - } - } else if ( - request.body === undefined || - Buffer.isBuffer(request.body) || - Object.keys(request.body).length === 0 - ) { - // parsing it again to make sure that the request is parsed atleast once by a json parser - let jsonParser = body_parser_1.json(); - let err = yield new Promise((resolve) => { - let resolvedCalled = false; - if (request.readable) { - jsonParser(request, new http_1.ServerResponse(request), (e) => { - if (!resolvedCalled) { - resolvedCalled = true; - resolve(e); - } - }); - } else { - resolve(undefined); - } - }); - if (err !== undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass a valid JSON input in the request body", - }); - } - } - } else if (method === "delete" || method === "get") { - if (request.query === undefined) { - let parser = body_parser_1.urlencoded({ extended: true }); - let err = yield new Promise((resolve) => parser(request, new http_1.ServerResponse(request), resolve)); - if (err !== undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass valid url encoded form in the request body", - }); - } - } - } - }); -} -exports.assertThatBodyParserHasBeenUsedForExpressLikeRequest = assertThatBodyParserHasBeenUsedForExpressLikeRequest; -function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(request) { - return __awaiter(this, void 0, void 0, function* () { - let parser = body_parser_1.urlencoded({ extended: true }); - let err = yield new Promise((resolve) => { - if (request.readable) { - parser(request, new http_1.ServerResponse(request), (e) => { - resolve(e); - }); - } else { - resolve(undefined); - } - }); - if (err !== undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass valid url encoded form in the request body", - }); - } - }); -} -exports.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest = assertFormDataBodyParserHasBeenUsedForExpressLikeRequest; -function setHeaderForExpressLikeResponse(res, key, value, allowDuplicateKey) { - try { - let existingHeaders = res.getHeaders(); - let existingValue = existingHeaders[key.toLowerCase()]; - // we have the res.header for compatibility with nextJS - if (existingValue === undefined) { - if (res.header !== undefined) { - res.header(key, value); - } else { - res.setHeader(key, value); - } - } else if (allowDuplicateKey) { - if (res.header !== undefined) { - res.header(key, existingValue + ", " + value); - } else { - res.setHeader(key, existingValue + ", " + value); - } - } else { - // we overwrite the current one with the new one - if (res.header !== undefined) { - res.header(key, value); - } else { - res.setHeader(key, value); - } - } - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } -} -exports.setHeaderForExpressLikeResponse = setHeaderForExpressLikeResponse; -/** - * - * @param res - * @param name - * @param value - * @param domain - * @param secure - * @param httpOnly - * @param expires - * @param path - */ -function setCookieForServerResponse(res, key, value, domain, secure, httpOnly, expires, path, sameSite) { - return appendToServerResponse( - res, - constants_1.COOKIE_HEADER, - serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite), - key - ); -} -exports.setCookieForServerResponse = setCookieForServerResponse; -/** - * Append additional header `field` with value `val`. - * - * Example: - * - * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); - * - * @param {ServerResponse} res - * @param {string} field - * @param {string| string[]} val - */ -function appendToServerResponse(res, field, val, key) { - let prev = res.getHeader(field); - res.setHeader(field, getCookieValueToSetInHeader(prev, val, key)); - return res; -} -function getCookieValueToSetInHeader(prev, val, key) { - let value = val; - if (prev !== undefined) { - // removing existing cookie with the same name - if (Array.isArray(prev)) { - let removedDuplicate = []; - for (let i = 0; i < prev.length; i++) { - let curr = prev[i]; - if (!curr.startsWith(key)) { - removedDuplicate.push(curr); - } - } - prev = removedDuplicate; - } else { - if (prev.startsWith(key)) { - prev = undefined; - } - } - if (prev !== undefined) { - value = Array.isArray(prev) ? prev.concat(val) : Array.isArray(val) ? [prev].concat(val) : [prev, val]; - } - } - value = Array.isArray(value) ? value.map(String) : String(value); - return value; -} -exports.getCookieValueToSetInHeader = getCookieValueToSetInHeader; -function serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite) { - let opts = { - domain, - secure, - httpOnly, - expires: new Date(expires), - path, - sameSite, - }; - return cookie_1.serialize(key, value, opts); -} -exports.serializeCookieValue = serializeCookieValue; diff --git a/lib/build/index.d.ts b/lib/build/index.d.ts deleted file mode 100644 index 96373acef..000000000 --- a/lib/build/index.d.ts +++ /dev/null @@ -1,93 +0,0 @@ -// @ts-nocheck -import SuperTokens from "./supertokens"; -import SuperTokensError from "./error"; -export default class SuperTokensWrapper { - static init: typeof SuperTokens.init; - static Error: typeof SuperTokensError; - static getAllCORSHeaders(): string[]; - static getUserCount(includeRecipeIds?: string[]): Promise; - static getUsersOldestFirst(input?: { - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - }): Promise<{ - users: { - recipeId: string; - user: any; - }[]; - nextPaginationToken?: string; - }>; - static getUsersNewestFirst(input?: { - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - }): Promise<{ - users: { - recipeId: string; - user: any; - }[]; - nextPaginationToken?: string; - }>; - static deleteUser( - userId: string - ): Promise<{ - status: "OK"; - }>; - static createUserIdMapping(input: { - superTokensUserId: string; - externalUserId: string; - externalUserIdInfo?: string; - force?: boolean; - }): Promise< - | { - status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; - } - | { - status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"; - doesSuperTokensUserIdExist: boolean; - doesExternalUserIdExist: boolean; - } - >; - static getUserIdMapping(input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - }): Promise< - | { - status: "OK"; - superTokensUserId: string; - externalUserId: string; - externalUserIdInfo: string | undefined; - } - | { - status: "UNKNOWN_MAPPING_ERROR"; - } - >; - static deleteUserIdMapping(input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - force?: boolean; - }): Promise<{ - status: "OK"; - didMappingExist: boolean; - }>; - static updateOrDeleteUserIdMappingInfo(input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - externalUserIdInfo?: string; - }): Promise<{ - status: "OK" | "UNKNOWN_MAPPING_ERROR"; - }>; -} -export declare let init: typeof SuperTokens.init; -export declare let getAllCORSHeaders: typeof SuperTokensWrapper.getAllCORSHeaders; -export declare let getUserCount: typeof SuperTokensWrapper.getUserCount; -export declare let getUsersOldestFirst: typeof SuperTokensWrapper.getUsersOldestFirst; -export declare let getUsersNewestFirst: typeof SuperTokensWrapper.getUsersNewestFirst; -export declare let deleteUser: typeof SuperTokensWrapper.deleteUser; -export declare let createUserIdMapping: typeof SuperTokensWrapper.createUserIdMapping; -export declare let getUserIdMapping: typeof SuperTokensWrapper.getUserIdMapping; -export declare let deleteUserIdMapping: typeof SuperTokensWrapper.deleteUserIdMapping; -export declare let updateOrDeleteUserIdMappingInfo: typeof SuperTokensWrapper.updateOrDeleteUserIdMappingInfo; -export declare let Error: typeof SuperTokensError; diff --git a/lib/build/index.js b/lib/build/index.js deleted file mode 100644 index d55623038..000000000 --- a/lib/build/index.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Error = exports.updateOrDeleteUserIdMappingInfo = exports.deleteUserIdMapping = exports.getUserIdMapping = exports.createUserIdMapping = exports.deleteUser = exports.getUsersNewestFirst = exports.getUsersOldestFirst = exports.getUserCount = exports.getAllCORSHeaders = exports.init = void 0; -const supertokens_1 = __importDefault(require("./supertokens")); -const error_1 = __importDefault(require("./error")); -// For Express -class SuperTokensWrapper { - static getAllCORSHeaders() { - return supertokens_1.default.getInstanceOrThrowError().getAllCORSHeaders(); - } - static getUserCount(includeRecipeIds) { - return supertokens_1.default.getInstanceOrThrowError().getUserCount(includeRecipeIds); - } - static getUsersOldestFirst(input) { - return supertokens_1.default - .getInstanceOrThrowError() - .getUsers(Object.assign({ timeJoinedOrder: "ASC" }, input)); - } - static getUsersNewestFirst(input) { - return supertokens_1.default - .getInstanceOrThrowError() - .getUsers(Object.assign({ timeJoinedOrder: "DESC" }, input)); - } - static deleteUser(userId) { - return supertokens_1.default.getInstanceOrThrowError().deleteUser({ - userId, - }); - } - static createUserIdMapping(input) { - return supertokens_1.default.getInstanceOrThrowError().createUserIdMapping(input); - } - static getUserIdMapping(input) { - return supertokens_1.default.getInstanceOrThrowError().getUserIdMapping(input); - } - static deleteUserIdMapping(input) { - return supertokens_1.default.getInstanceOrThrowError().deleteUserIdMapping(input); - } - static updateOrDeleteUserIdMappingInfo(input) { - return supertokens_1.default.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo(input); - } -} -exports.default = SuperTokensWrapper; -SuperTokensWrapper.init = supertokens_1.default.init; -SuperTokensWrapper.Error = error_1.default; -exports.init = SuperTokensWrapper.init; -exports.getAllCORSHeaders = SuperTokensWrapper.getAllCORSHeaders; -exports.getUserCount = SuperTokensWrapper.getUserCount; -exports.getUsersOldestFirst = SuperTokensWrapper.getUsersOldestFirst; -exports.getUsersNewestFirst = SuperTokensWrapper.getUsersNewestFirst; -exports.deleteUser = SuperTokensWrapper.deleteUser; -exports.createUserIdMapping = SuperTokensWrapper.createUserIdMapping; -exports.getUserIdMapping = SuperTokensWrapper.getUserIdMapping; -exports.deleteUserIdMapping = SuperTokensWrapper.deleteUserIdMapping; -exports.updateOrDeleteUserIdMappingInfo = SuperTokensWrapper.updateOrDeleteUserIdMappingInfo; -exports.Error = SuperTokensWrapper.Error; diff --git a/lib/build/ingredients/emaildelivery/index.d.ts b/lib/build/ingredients/emaildelivery/index.d.ts deleted file mode 100644 index bfa1e8fd9..000000000 --- a/lib/build/ingredients/emaildelivery/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { TypeInputWithService, EmailDeliveryInterface } from "./types"; -export default class EmailDelivery { - ingredientInterfaceImpl: EmailDeliveryInterface; - constructor(config: TypeInputWithService); -} diff --git a/lib/build/ingredients/emaildelivery/index.js b/lib/build/ingredients/emaildelivery/index.js deleted file mode 100644 index 958cd35a2..000000000 --- a/lib/build/ingredients/emaildelivery/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -class EmailDelivery { - constructor(config) { - let builder = new supertokens_js_override_1.default(config.service); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.ingredientInterfaceImpl = builder.build(); - } -} -exports.default = EmailDelivery; diff --git a/lib/build/ingredients/emaildelivery/services/smtp.d.ts b/lib/build/ingredients/emaildelivery/services/smtp.d.ts deleted file mode 100644 index 43a3c5437..000000000 --- a/lib/build/ingredients/emaildelivery/services/smtp.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -export interface SMTPServiceConfig { - host: string; - from: { - name: string; - email: string; - }; - port: number; - secure?: boolean; - authUsername?: string; - password: string; -} -export interface GetContentResult { - body: string; - isHtml: boolean; - subject: string; - toEmail: string; -} -export declare type TypeInputSendRawEmail = GetContentResult & { - userContext: any; -}; -export declare type ServiceInterface = { - sendRawEmail: (input: TypeInputSendRawEmail) => Promise; - getContent: ( - input: T & { - userContext: any; - } - ) => Promise; -}; -export declare type TypeInput = { - smtpSettings: SMTPServiceConfig; - override?: (oI: ServiceInterface, builder: OverrideableBuilder>) => ServiceInterface; -}; diff --git a/lib/build/ingredients/emaildelivery/services/smtp.js b/lib/build/ingredients/emaildelivery/services/smtp.js deleted file mode 100644 index c8ad2e549..000000000 --- a/lib/build/ingredients/emaildelivery/services/smtp.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/ingredients/emaildelivery/types.d.ts b/lib/build/ingredients/emaildelivery/types.d.ts deleted file mode 100644 index 494e9f4cd..000000000 --- a/lib/build/ingredients/emaildelivery/types.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -export declare type EmailDeliveryInterface = { - sendEmail: ( - input: T & { - userContext: any; - } - ) => Promise; -}; -/** - * config class parameter when parent Recipe create a new EmailDeliveryIngredient object via constructor - */ -export interface TypeInput { - service?: EmailDeliveryInterface; - override?: ( - originalImplementation: EmailDeliveryInterface, - builder: OverrideableBuilder> - ) => EmailDeliveryInterface; -} -export interface TypeInputWithService { - service: EmailDeliveryInterface; - override?: ( - originalImplementation: EmailDeliveryInterface, - builder: OverrideableBuilder> - ) => EmailDeliveryInterface; -} diff --git a/lib/build/ingredients/emaildelivery/types.js b/lib/build/ingredients/emaildelivery/types.js deleted file mode 100644 index c8ad2e549..000000000 --- a/lib/build/ingredients/emaildelivery/types.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/ingredients/smsdelivery/index.d.ts b/lib/build/ingredients/smsdelivery/index.d.ts deleted file mode 100644 index 80e9f60a6..000000000 --- a/lib/build/ingredients/smsdelivery/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { TypeInputWithService, SmsDeliveryInterface } from "./types"; -export default class SmsDelivery { - ingredientInterfaceImpl: SmsDeliveryInterface; - constructor(config: TypeInputWithService); -} diff --git a/lib/build/ingredients/smsdelivery/index.js b/lib/build/ingredients/smsdelivery/index.js deleted file mode 100644 index a52608dc0..000000000 --- a/lib/build/ingredients/smsdelivery/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -class SmsDelivery { - constructor(config) { - let builder = new supertokens_js_override_1.default(config.service); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.ingredientInterfaceImpl = builder.build(); - } -} -exports.default = SmsDelivery; diff --git a/lib/build/ingredients/smsdelivery/services/supertokens.d.ts b/lib/build/ingredients/smsdelivery/services/supertokens.d.ts deleted file mode 100644 index 1196903bb..000000000 --- a/lib/build/ingredients/smsdelivery/services/supertokens.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-nocheck -export declare const SUPERTOKENS_SMS_SERVICE_URL = "https://api.supertokens.com/0/services/sms"; diff --git a/lib/build/ingredients/smsdelivery/services/supertokens.js b/lib/build/ingredients/smsdelivery/services/supertokens.js deleted file mode 100644 index 2689f25fb..000000000 --- a/lib/build/ingredients/smsdelivery/services/supertokens.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SUPERTOKENS_SMS_SERVICE_URL = void 0; -exports.SUPERTOKENS_SMS_SERVICE_URL = "https://api.supertokens.com/0/services/sms"; diff --git a/lib/build/ingredients/smsdelivery/services/twilio.d.ts b/lib/build/ingredients/smsdelivery/services/twilio.d.ts deleted file mode 100644 index d2d0dabae..000000000 --- a/lib/build/ingredients/smsdelivery/services/twilio.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -import { ClientOpts } from "twilio/lib/base/BaseTwilio"; -/** - * only one of "from" and "messagingServiceSid" should be passed. - * if both are passed, we should throw error to the user - * saying that only one of them should be set. this is because - * both parameters can't be passed while calling twilio API. - * if none of "from" and "messagingServiceSid" is passed, error - * should be thrown. - */ -export declare type TwilioServiceConfig = - | { - accountSid: string; - authToken: string; - from: string; - opts?: ClientOpts; - } - | { - accountSid: string; - authToken: string; - messagingServiceSid: string; - opts?: ClientOpts; - }; -export interface GetContentResult { - body: string; - toPhoneNumber: string; -} -export declare type TypeInputSendRawSms = GetContentResult & { - userContext: any; -} & ( - | { - from: string; - } - | { - messagingServiceSid: string; - } - ); -export declare type ServiceInterface = { - sendRawSms: (input: TypeInputSendRawSms) => Promise; - getContent: ( - input: T & { - userContext: any; - } - ) => Promise; -}; -export declare type TypeInput = { - twilioSettings: TwilioServiceConfig; - override?: (oI: ServiceInterface, builder: OverrideableBuilder>) => ServiceInterface; -}; -export declare function normaliseUserInputConfig(input: TypeInput): TypeInput; diff --git a/lib/build/ingredients/smsdelivery/services/twilio.js b/lib/build/ingredients/smsdelivery/services/twilio.js deleted file mode 100644 index 73876eb37..000000000 --- a/lib/build/ingredients/smsdelivery/services/twilio.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.normaliseUserInputConfig = void 0; -function normaliseUserInputConfig(input) { - let from = "from" in input.twilioSettings ? input.twilioSettings.from : undefined; - let messagingServiceSid = - "messagingServiceSid" in input.twilioSettings ? input.twilioSettings.messagingServiceSid : undefined; - if ( - (from === undefined && messagingServiceSid === undefined) || - (from !== undefined && messagingServiceSid !== undefined) - ) { - throw Error(`Please pass exactly one of "from" and "messagingServiceSid" config for twilioSettings.`); - } - return input; -} -exports.normaliseUserInputConfig = normaliseUserInputConfig; diff --git a/lib/build/ingredients/smsdelivery/types.d.ts b/lib/build/ingredients/smsdelivery/types.d.ts deleted file mode 100644 index f45dc8f2a..000000000 --- a/lib/build/ingredients/smsdelivery/types.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -export declare type SmsDeliveryInterface = { - sendSms: ( - input: T & { - userContext: any; - } - ) => Promise; -}; -/** - * config class parameter when parent Recipe create a new SmsDeliveryIngredient object via constructor - */ -export interface TypeInput { - service?: SmsDeliveryInterface; - override?: ( - originalImplementation: SmsDeliveryInterface, - builder: OverrideableBuilder> - ) => SmsDeliveryInterface; -} -export interface TypeInputWithService { - service: SmsDeliveryInterface; - override?: ( - originalImplementation: SmsDeliveryInterface, - builder: OverrideableBuilder> - ) => SmsDeliveryInterface; -} diff --git a/lib/build/ingredients/smsdelivery/types.js b/lib/build/ingredients/smsdelivery/types.js deleted file mode 100644 index c8ad2e549..000000000 --- a/lib/build/ingredients/smsdelivery/types.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/logger.d.ts b/lib/build/logger.d.ts deleted file mode 100644 index 1a3a6aef6..000000000 --- a/lib/build/logger.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -declare function logDebugMessage(message: string): void; -export { logDebugMessage }; diff --git a/lib/build/logger.js b/lib/build/logger.js deleted file mode 100644 index 520e67fc5..000000000 --- a/lib/build/logger.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.logDebugMessage = void 0; -const debug_1 = __importDefault(require("debug")); -const version_1 = require("./version"); -const SUPERTOKENS_DEBUG_NAMESPACE = "com.supertokens"; -/* - The debug logger below can be used to log debug messages in the following format - com.supertokens {t: "2022-03-18T11:15:24.608Z", message: Your message, file: "/home/supertokens-node/lib/build/supertokens.js:231:18" sdkVer: "9.2.0"} +0m -*/ -function logDebugMessage(message) { - if (debug_1.default.enabled(SUPERTOKENS_DEBUG_NAMESPACE)) { - debug_1.default(SUPERTOKENS_DEBUG_NAMESPACE)( - `{t: "${new Date().toISOString()}", message: \"${message}\", file: \"${getFileLocation()}\" sdkVer: "${ - version_1.version - }"}` - ); - console.log(); - } -} -exports.logDebugMessage = logDebugMessage; -let getFileLocation = () => { - let errorObject = new Error(); - if (errorObject.stack === undefined) { - // should not come here - return "N/A"; - } - // split the error stack into an array with new line as the separator - let errorStack = errorObject.stack.split("\n"); - // find return the first trace which doesnt have the logger.js file - for (let i = 1; i < errorStack.length; i++) { - if (!errorStack[i].includes("logger.js")) { - // retrieve the string between the parenthesis - return errorStack[i].match(/(?<=\().+?(?=\))/g); - } - } - return "N/A"; -}; diff --git a/lib/build/nextjs.d.ts b/lib/build/nextjs.d.ts deleted file mode 100644 index 195a81422..000000000 --- a/lib/build/nextjs.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -export default class NextJS { - static superTokensNextWrapper( - middleware: (next: (middlewareError?: any) => void) => Promise, - request: any, - response: any - ): Promise; -} -export declare let superTokensNextWrapper: typeof NextJS.superTokensNextWrapper; diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js deleted file mode 100644 index 97f8e2bad..000000000 --- a/lib/build/nextjs.js +++ /dev/null @@ -1,94 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.superTokensNextWrapper = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const express_1 = require("./framework/express"); -function next(request, response, resolve, reject) { - return function (middlewareError) { - return __awaiter(this, void 0, void 0, function* () { - if (middlewareError === undefined) { - return resolve(); - } - yield express_1.errorHandler()(middlewareError, request, response, (errorHandlerError) => { - if (errorHandlerError !== undefined) { - return reject(errorHandlerError); - } - // do nothing, error handler does not resolve the promise. - }); - }); - }; -} -class NextJS { - static superTokensNextWrapper(middleware, request, response) { - return __awaiter(this, void 0, void 0, function* () { - return new Promise((resolve, reject) => - __awaiter(this, void 0, void 0, function* () { - request.__supertokensFromNextJS = true; - try { - let callbackCalled = false; - const result = yield middleware((err) => { - callbackCalled = true; - next(request, response, resolve, reject)(err); - }); - if (!callbackCalled && !response.finished && !response.headersSent) { - return resolve(result); - } - } catch (err) { - yield express_1.errorHandler()(err, request, response, (errorHandlerError) => { - if (errorHandlerError !== undefined) { - return reject(errorHandlerError); - } - // do nothing, error handler does not resolve the promise. - }); - } - }) - ); - }); - } -} -exports.default = NextJS; -exports.superTokensNextWrapper = NextJS.superTokensNextWrapper; diff --git a/lib/build/normalisedURLDomain.d.ts b/lib/build/normalisedURLDomain.d.ts deleted file mode 100644 index b75c15d4b..000000000 --- a/lib/build/normalisedURLDomain.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -export default class NormalisedURLDomain { - private value; - constructor(url: string); - getAsStringDangerous: () => string; -} diff --git a/lib/build/normalisedURLDomain.js b/lib/build/normalisedURLDomain.js deleted file mode 100644 index e4a69f7f8..000000000 --- a/lib/build/normalisedURLDomain.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const url_1 = require("url"); -const utils_1 = require("./utils"); -class NormalisedURLDomain { - constructor(url) { - this.getAsStringDangerous = () => { - return this.value; - }; - this.value = normaliseURLDomainOrThrowError(url); - } -} -exports.default = NormalisedURLDomain; -function normaliseURLDomainOrThrowError(input, ignoreProtocol = false) { - input = input.trim().toLowerCase(); - try { - if (!input.startsWith("http://") && !input.startsWith("https://") && !input.startsWith("supertokens://")) { - throw new Error("converting to proper URL"); - } - let urlObj = new url_1.URL(input); - if (ignoreProtocol) { - if (urlObj.hostname.startsWith("localhost") || utils_1.isAnIpAddress(urlObj.hostname)) { - input = "http://" + urlObj.host; - } else { - input = "https://" + urlObj.host; - } - } else { - input = urlObj.protocol + "//" + urlObj.host; - } - return input; - } catch (err) {} - // not a valid URL - if (input.startsWith("/")) { - throw Error("Please provide a valid domain name"); - } - if (input.indexOf(".") === 0) { - input = input.substr(1); - } - // If the input contains a . it means they have given a domain name. - // So we try assuming that they have given a domain name - if ( - (input.indexOf(".") !== -1 || input.startsWith("localhost")) && - !input.startsWith("http://") && - !input.startsWith("https://") - ) { - input = "https://" + input; - // at this point, it should be a valid URL. So we test that before doing a recursive call - try { - new url_1.URL(input); - return normaliseURLDomainOrThrowError(input, true); - } catch (err) {} - } - throw Error("Please provide a valid domain name"); -} diff --git a/lib/build/normalisedURLPath.d.ts b/lib/build/normalisedURLPath.d.ts deleted file mode 100644 index db7787bb4..000000000 --- a/lib/build/normalisedURLPath.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -export default class NormalisedURLPath { - private value; - constructor(url: string); - startsWith: (other: NormalisedURLPath) => boolean; - appendPath: (other: NormalisedURLPath) => NormalisedURLPath; - getAsStringDangerous: () => string; - equals: (other: NormalisedURLPath) => boolean; - isARecipePath: () => boolean; -} diff --git a/lib/build/normalisedURLPath.js b/lib/build/normalisedURLPath.js deleted file mode 100644 index c254d8349..000000000 --- a/lib/build/normalisedURLPath.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const url_1 = require("url"); -class NormalisedURLPath { - constructor(url) { - this.startsWith = (other) => { - return this.value.startsWith(other.value); - }; - this.appendPath = (other) => { - return new NormalisedURLPath(this.value + other.value); - }; - this.getAsStringDangerous = () => { - return this.value; - }; - this.equals = (other) => { - return this.value === other.value; - }; - this.isARecipePath = () => { - return this.value === "/recipe" || this.value.startsWith("/recipe/"); - }; - this.value = normaliseURLPathOrThrowError(url); - } -} -exports.default = NormalisedURLPath; -function normaliseURLPathOrThrowError(input) { - input = input.trim().toLowerCase(); - try { - if (!input.startsWith("http://") && !input.startsWith("https://")) { - throw new Error("converting to proper URL"); - } - let urlObj = new url_1.URL(input); - input = urlObj.pathname; - if (input.charAt(input.length - 1) === "/") { - return input.substr(0, input.length - 1); - } - return input; - } catch (err) {} - // not a valid URL - // If the input contains a . it means they have given a domain name. - // So we try assuming that they have given a domain name + path - if ( - (domainGiven(input) || input.startsWith("localhost")) && - !input.startsWith("http://") && - !input.startsWith("https://") - ) { - input = "http://" + input; - return normaliseURLPathOrThrowError(input); - } - if (input.charAt(0) !== "/") { - input = "/" + input; - } - // at this point, we should be able to convert it into a fake URL and recursively call this function. - try { - // test that we can convert this to prevent an infinite loop - new url_1.URL("http://example.com" + input); - return normaliseURLPathOrThrowError("http://example.com" + input); - } catch (err) { - throw Error("Please provide a valid URL path"); - } -} -function domainGiven(input) { - // If no dot, return false. - if (input.indexOf(".") === -1 || input.startsWith("/")) { - return false; - } - try { - let url = new url_1.URL(input); - return url.hostname.indexOf(".") !== -1; - } catch (ignored) {} - try { - let url = new url_1.URL("http://" + input); - return url.hostname.indexOf(".") !== -1; - } catch (ignored) {} - return false; -} diff --git a/lib/build/postSuperTokensInitCallbacks.d.ts b/lib/build/postSuperTokensInitCallbacks.d.ts deleted file mode 100644 index 6eef30adb..000000000 --- a/lib/build/postSuperTokensInitCallbacks.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -export declare class PostSuperTokensInitCallbacks { - static postInitCallbacks: (() => void)[]; - static addPostInitCallback(cb: () => void): void; - static runPostInitCallbacks(): void; -} diff --git a/lib/build/postSuperTokensInitCallbacks.js b/lib/build/postSuperTokensInitCallbacks.js deleted file mode 100644 index a58af9157..000000000 --- a/lib/build/postSuperTokensInitCallbacks.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PostSuperTokensInitCallbacks = void 0; -class PostSuperTokensInitCallbacks { - static addPostInitCallback(cb) { - PostSuperTokensInitCallbacks.postInitCallbacks.push(cb); - } - static runPostInitCallbacks() { - for (const cb of PostSuperTokensInitCallbacks.postInitCallbacks) { - cb(); - } - PostSuperTokensInitCallbacks.postInitCallbacks = []; - } -} -exports.PostSuperTokensInitCallbacks = PostSuperTokensInitCallbacks; -PostSuperTokensInitCallbacks.postInitCallbacks = []; diff --git a/lib/build/processState.d.ts b/lib/build/processState.d.ts deleted file mode 100644 index aa2d9a2a8..000000000 --- a/lib/build/processState.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -export declare enum PROCESS_STATE { - CALLING_SERVICE_IN_VERIFY = 0, - CALLING_SERVICE_IN_GET_HANDSHAKE_INFO = 1, - CALLING_SERVICE_IN_GET_API_VERSION = 2, - CALLING_SERVICE_IN_REQUEST_HELPER = 3, -} -export declare class ProcessState { - history: PROCESS_STATE[]; - private static instance; - private constructor(); - static getInstance(): ProcessState; - addState: (state: PROCESS_STATE) => void; - private getEventByLastEventByName; - reset: () => void; - waitForEvent: (state: PROCESS_STATE, timeInMS?: number) => Promise; -} diff --git a/lib/build/processState.js b/lib/build/processState.js deleted file mode 100644 index cc8358844..000000000 --- a/lib/build/processState.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ProcessState = exports.PROCESS_STATE = void 0; -var PROCESS_STATE; -(function (PROCESS_STATE) { - PROCESS_STATE[(PROCESS_STATE["CALLING_SERVICE_IN_VERIFY"] = 0)] = "CALLING_SERVICE_IN_VERIFY"; - PROCESS_STATE[(PROCESS_STATE["CALLING_SERVICE_IN_GET_HANDSHAKE_INFO"] = 1)] = - "CALLING_SERVICE_IN_GET_HANDSHAKE_INFO"; - PROCESS_STATE[(PROCESS_STATE["CALLING_SERVICE_IN_GET_API_VERSION"] = 2)] = "CALLING_SERVICE_IN_GET_API_VERSION"; - PROCESS_STATE[(PROCESS_STATE["CALLING_SERVICE_IN_REQUEST_HELPER"] = 3)] = "CALLING_SERVICE_IN_REQUEST_HELPER"; -})((PROCESS_STATE = exports.PROCESS_STATE || (exports.PROCESS_STATE = {}))); -class ProcessState { - constructor() { - this.history = []; - this.addState = (state) => { - if (process.env.TEST_MODE === "testing") { - this.history.push(state); - } - }; - this.getEventByLastEventByName = (state) => { - for (let i = this.history.length - 1; i >= 0; i--) { - if (this.history[i] === state) { - return this.history[i]; - } - } - return undefined; - }; - this.reset = () => { - this.history = []; - }; - this.waitForEvent = (state, timeInMS = 7000) => - __awaiter(this, void 0, void 0, function* () { - let startTime = Date.now(); - return new Promise((resolve) => { - let actualThis = this; - function tryAndGet() { - let result = actualThis.getEventByLastEventByName(state); - if (result === undefined) { - if (Date.now() - startTime > timeInMS) { - resolve(undefined); - } else { - setTimeout(tryAndGet, 1000); - } - } else { - resolve(result); - } - } - tryAndGet(); - }); - }); - } - static getInstance() { - if (ProcessState.instance === undefined) { - ProcessState.instance = new ProcessState(); - } - return ProcessState.instance; - } -} -exports.ProcessState = ProcessState; diff --git a/lib/build/querier.d.ts b/lib/build/querier.d.ts deleted file mode 100644 index c11cf7e16..000000000 --- a/lib/build/querier.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -import NormalisedURLDomain from "./normalisedURLDomain"; -import NormalisedURLPath from "./normalisedURLPath"; -export declare class Querier { - private static initCalled; - private static hosts; - private static apiKey; - private static apiVersion; - private static lastTriedIndex; - private static hostsAliveForTesting; - private __hosts; - private rIdToCore; - private constructor(); - getAPIVersion: () => Promise; - static reset(): void; - getHostsAliveForTesting: () => Set; - static getNewInstanceOrThrowError(rIdToCore?: string): Querier; - static init( - hosts?: { - domain: NormalisedURLDomain; - basePath: NormalisedURLPath; - }[], - apiKey?: string - ): void; - sendPostRequest: (path: NormalisedURLPath, body: any) => Promise; - sendDeleteRequest: (path: NormalisedURLPath, body: any, params?: any) => Promise; - sendGetRequest: (path: NormalisedURLPath, params: any) => Promise; - sendPutRequest: (path: NormalisedURLPath, body: any) => Promise; - private sendRequestHelper; -} diff --git a/lib/build/querier.js b/lib/build/querier.js deleted file mode 100644 index a0c2a3d16..000000000 --- a/lib/build/querier.js +++ /dev/null @@ -1,306 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Querier = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const axios_1 = __importDefault(require("axios")); -const utils_1 = require("./utils"); -const version_1 = require("./version"); -const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); -const processState_1 = require("./processState"); -class Querier { - // we have rIdToCore so that recipes can force change the rId sent to core. This is a hack until the core is able - // to support multiple rIds per API - constructor(hosts, rIdToCore) { - this.getAPIVersion = () => - __awaiter(this, void 0, void 0, function* () { - var _a; - if (Querier.apiVersion !== undefined) { - return Querier.apiVersion; - } - processState_1.ProcessState.getInstance().addState( - processState_1.PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION - ); - let response = yield this.sendRequestHelper( - new normalisedURLPath_1.default("/apiversion"), - "GET", - (url) => { - let headers = {}; - if (Querier.apiKey !== undefined) { - headers = { - "api-key": Querier.apiKey, - }; - } - return axios_1.default.get(url, { - headers, - }); - }, - ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 - ); - let cdiSupportedByServer = response.versions; - let supportedVersion = utils_1.getLargestVersionFromIntersection( - cdiSupportedByServer, - version_1.cdiSupported - ); - if (supportedVersion === undefined) { - throw Error( - "The running SuperTokens core version is not compatible with this NodeJS SDK. Please visit https://supertokens.io/docs/community/compatibility to find the right versions" - ); - } - Querier.apiVersion = supportedVersion; - return Querier.apiVersion; - }); - this.getHostsAliveForTesting = () => { - if (process.env.TEST_MODE !== "testing") { - throw Error("calling testing function in non testing env"); - } - return Querier.hostsAliveForTesting; - }; - // path should start with "/" - this.sendPostRequest = (path, body) => - __awaiter(this, void 0, void 0, function* () { - var _b; - return this.sendRequestHelper( - path, - "POST", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { - "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", - }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - return yield axios_1.default({ - method: "POST", - url, - data: body, - headers, - }); - }), - ((_b = this.__hosts) === null || _b === void 0 ? void 0 : _b.length) || 0 - ); - }); - // path should start with "/" - this.sendDeleteRequest = (path, body, params) => - __awaiter(this, void 0, void 0, function* () { - var _c; - return this.sendRequestHelper( - path, - "DELETE", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { "cdi-version": apiVersion }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { - "api-key": Querier.apiKey, - "content-type": "application/json; charset=utf-8", - }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - return yield axios_1.default({ - method: "DELETE", - url, - data: body, - headers, - params, - }); - }), - ((_c = this.__hosts) === null || _c === void 0 ? void 0 : _c.length) || 0 - ); - }); - // path should start with "/" - this.sendGetRequest = (path, params) => - __awaiter(this, void 0, void 0, function* () { - var _d; - return this.sendRequestHelper( - path, - "GET", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { "cdi-version": apiVersion }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - return yield axios_1.default.get(url, { - params, - headers, - }); - }), - ((_d = this.__hosts) === null || _d === void 0 ? void 0 : _d.length) || 0 - ); - }); - // path should start with "/" - this.sendPutRequest = (path, body) => - __awaiter(this, void 0, void 0, function* () { - var _e; - return this.sendRequestHelper( - path, - "PUT", - (url) => - __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield this.getAPIVersion(); - let headers = { - "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", - }; - if (Querier.apiKey !== undefined) { - headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); - } - return yield axios_1.default({ - method: "PUT", - url, - data: body, - headers, - }); - }), - ((_e = this.__hosts) === null || _e === void 0 ? void 0 : _e.length) || 0 - ); - }); - // path should start with "/" - this.sendRequestHelper = (path, method, axiosFunction, numberOfTries) => - __awaiter(this, void 0, void 0, function* () { - if (this.__hosts === undefined) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - if (numberOfTries === 0) { - throw Error("No SuperTokens core available to query"); - } - let currentDomain = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); - let currentBasePath = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); - Querier.lastTriedIndex++; - Querier.lastTriedIndex = Querier.lastTriedIndex % this.__hosts.length; - try { - processState_1.ProcessState.getInstance().addState( - processState_1.PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER - ); - let response = yield axiosFunction(currentDomain + currentBasePath + path.getAsStringDangerous()); - if (process.env.TEST_MODE === "testing") { - Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); - } - if (response.status !== 200) { - throw response; - } - return response.data; - } catch (err) { - if (err.message !== undefined && err.message.includes("ECONNREFUSED")) { - return yield this.sendRequestHelper(path, method, axiosFunction, numberOfTries - 1); - } - if ( - err.response !== undefined && - err.response.status !== undefined && - err.response.data !== undefined - ) { - throw new Error( - "SuperTokens core threw an error for a " + - method + - " request to path: '" + - path.getAsStringDangerous() + - "' with status code: " + - err.response.status + - " and message: " + - err.response.data - ); - } else { - throw err; - } - } - }); - this.__hosts = hosts; - this.rIdToCore = rIdToCore; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw Error("calling testing function in non testing env"); - } - Querier.initCalled = false; - } - static getNewInstanceOrThrowError(rIdToCore) { - if (!Querier.initCalled) { - throw Error("Please call the supertokens.init function before using SuperTokens"); - } - return new Querier(Querier.hosts, rIdToCore); - } - static init(hosts, apiKey) { - if (!Querier.initCalled) { - Querier.initCalled = true; - Querier.hosts = hosts; - Querier.apiKey = apiKey; - Querier.apiVersion = undefined; - Querier.lastTriedIndex = 0; - Querier.hostsAliveForTesting = new Set(); - } - } -} -exports.Querier = Querier; -Querier.initCalled = false; -Querier.hosts = undefined; -Querier.apiKey = undefined; -Querier.apiVersion = undefined; -Querier.lastTriedIndex = 0; -Querier.hostsAliveForTesting = new Set(); diff --git a/lib/build/recipe/dashboard/api/analytics.d.ts b/lib/build/recipe/dashboard/api/analytics.d.ts deleted file mode 100644 index 32bbb6d19..000000000 --- a/lib/build/recipe/dashboard/api/analytics.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export declare type Response = { - status: "OK"; -}; -export default function analyticsPost(_: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/dashboard/api/analytics.js b/lib/build/recipe/dashboard/api/analytics.js deleted file mode 100644 index 0da73942f..000000000 --- a/lib/build/recipe/dashboard/api/analytics.js +++ /dev/null @@ -1,124 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const supertokens_1 = __importDefault(require("../../../supertokens")); -const querier_1 = require("../../../querier"); -const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -const version_1 = require("../../../version"); -const error_1 = __importDefault(require("../../../error")); -const axios_1 = __importDefault(require("axios")); -function analyticsPost(_, options) { - return __awaiter(this, void 0, void 0, function* () { - // If telemetry is disabled, dont send any event - if (!supertokens_1.default.getInstanceOrThrowError().telemetryEnabled) { - return { - status: "OK", - }; - } - const { email, dashboardVersion } = yield options.req.getJSONBody(); - if (email === undefined) { - throw new error_1.default({ - message: "Missing required property 'email'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (dashboardVersion === undefined) { - throw new error_1.default({ - message: "Missing required property 'dashboardVersion'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let telemetryId; - let numberOfUsers; - try { - let querier = querier_1.Querier.getNewInstanceOrThrowError(options.recipeId); - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/telemetry"), {}); - if (response.exists) { - telemetryId = response.telemetryId; - } - numberOfUsers = yield supertokens_1.default.getInstanceOrThrowError().getUserCount(); - } catch (_) { - // If either telemetry id API or user count fetch fails, no event should be sent - return { - status: "OK", - }; - } - const { apiDomain, websiteDomain, appName } = options.appInfo; - const data = { - websiteDomain: websiteDomain.getAsStringDangerous(), - apiDomain: apiDomain.getAsStringDangerous(), - appName, - sdk: "node", - sdkVersion: version_1.version, - telemetryId, - numberOfUsers, - email, - dashboardVersion, - }; - try { - yield axios_1.default({ - url: "https://api.supertokens.com/0/st/telemetry", - method: "POST", - data, - headers: { - "api-version": 3, - }, - }); - } catch (e) { - // Ignored - } - return { - status: "OK", - }; - }); -} -exports.default = analyticsPost; diff --git a/lib/build/recipe/dashboard/api/apiKeyProtector.d.ts b/lib/build/recipe/dashboard/api/apiKeyProtector.d.ts deleted file mode 100644 index ccb15887b..000000000 --- a/lib/build/recipe/dashboard/api/apiKeyProtector.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { APIFunction, APIInterface, APIOptions } from "../types"; -export default function apiKeyProtector( - apiImplementation: APIInterface, - options: APIOptions, - apiFunction: APIFunction -): Promise; diff --git a/lib/build/recipe/dashboard/api/apiKeyProtector.js b/lib/build/recipe/dashboard/api/apiKeyProtector.js deleted file mode 100644 index 14f446112..000000000 --- a/lib/build/recipe/dashboard/api/apiKeyProtector.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const utils_1 = require("../../../utils"); -const utils_2 = require("../utils"); -function apiKeyProtector(apiImplementation, options, apiFunction) { - return __awaiter(this, void 0, void 0, function* () { - const shouldAllowAccess = yield options.recipeImplementation.shouldAllowAccess({ - req: options.req, - config: options.config, - userContext: utils_1.makeDefaultUserContextFromAPI(options.req), - }); - if (!shouldAllowAccess) { - utils_2.sendUnauthorisedAccess(options.res); - return true; - } - const response = yield apiFunction(apiImplementation, options); - options.res.sendJSONResponse(response); - return true; - }); -} -exports.default = apiKeyProtector; diff --git a/lib/build/recipe/dashboard/api/dashboard.d.ts b/lib/build/recipe/dashboard/api/dashboard.d.ts deleted file mode 100644 index f6bd1a1bf..000000000 --- a/lib/build/recipe/dashboard/api/dashboard.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export default function dashboard(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/dashboard/api/dashboard.js b/lib/build/recipe/dashboard/api/dashboard.js deleted file mode 100644 index 5d61bede0..000000000 --- a/lib/build/recipe/dashboard/api/dashboard.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -function dashboard(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.dashboardGET === undefined) { - return false; - } - const htmlString = yield apiImplementation.dashboardGET({ - options, - userContext: utils_1.makeDefaultUserContextFromAPI(options.req), - }); - options.res.sendHTMLResponse(htmlString); - return true; - }); -} -exports.default = dashboard; diff --git a/lib/build/recipe/dashboard/api/implementation.d.ts b/lib/build/recipe/dashboard/api/implementation.d.ts deleted file mode 100644 index 0218549fa..000000000 --- a/lib/build/recipe/dashboard/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../types"; -export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/dashboard/api/implementation.js b/lib/build/recipe/dashboard/api/implementation.js deleted file mode 100644 index 730bcc1b0..000000000 --- a/lib/build/recipe/dashboard/api/implementation.js +++ /dev/null @@ -1,110 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLDomain_1 = __importDefault(require("../../../normalisedURLDomain")); -const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -const querier_1 = require("../../../querier"); -const supertokens_1 = __importDefault(require("../../../supertokens")); -const utils_1 = require("../../../utils"); -const constants_1 = require("../constants"); -function getAPIImplementation() { - return { - dashboardGET: function (input) { - return __awaiter(this, void 0, void 0, function* () { - const bundleBasePathString = yield input.options.recipeImplementation.getDashboardBundleLocation({ - userContext: input.userContext, - }); - const bundleDomain = - new normalisedURLDomain_1.default(bundleBasePathString).getAsStringDangerous() + - new normalisedURLPath_1.default(bundleBasePathString).getAsStringDangerous(); - let connectionURI = ""; - const superTokensInstance = supertokens_1.default.getInstanceOrThrowError(); - const authMode = input.options.config.authMode; - if (superTokensInstance.supertokens !== undefined) { - connectionURI = superTokensInstance.supertokens.connectionURI; - } - let isSearchEnabled = false; - const cdiVersion = yield querier_1.Querier.getNewInstanceOrThrowError( - input.options.recipeId - ).getAPIVersion(); - if (utils_1.maxVersion("2.20", cdiVersion) === cdiVersion) { - // Only enable search if CDI version is 2.20 or above - isSearchEnabled = true; - } - return ` - - - - - - - - - - -
- - - `; - }); - }, - }; -} -exports.default = getAPIImplementation; diff --git a/lib/build/recipe/dashboard/api/search/tagsGet.d.ts b/lib/build/recipe/dashboard/api/search/tagsGet.d.ts deleted file mode 100644 index a8a374452..000000000 --- a/lib/build/recipe/dashboard/api/search/tagsGet.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type TagsResponse = { - status: "OK"; - tags: string[]; -}; -export declare const getSearchTags: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/search/tagsGet.js b/lib/build/recipe/dashboard/api/search/tagsGet.js deleted file mode 100644 index 60d752f92..000000000 --- a/lib/build/recipe/dashboard/api/search/tagsGet.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getSearchTags = void 0; -const querier_1 = require("../../../../querier"); -const normalisedURLPath_1 = __importDefault(require("../../../../normalisedURLPath")); -const getSearchTags = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(options.recipeId); - let tagsResponse = yield querier.sendGetRequest(new normalisedURLPath_1.default("/user/search/tags"), {}); - return tagsResponse; - }); -exports.getSearchTags = getSearchTags; diff --git a/lib/build/recipe/dashboard/api/signIn.d.ts b/lib/build/recipe/dashboard/api/signIn.d.ts deleted file mode 100644 index 899d506d9..000000000 --- a/lib/build/recipe/dashboard/api/signIn.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export default function signIn(_: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/dashboard/api/signIn.js b/lib/build/recipe/dashboard/api/signIn.js deleted file mode 100644 index e78ba8aa4..000000000 --- a/lib/build/recipe/dashboard/api/signIn.js +++ /dev/null @@ -1,84 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../../../error")); -const querier_1 = require("../../../querier"); -const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -function signIn(_, options) { - return __awaiter(this, void 0, void 0, function* () { - const { email, password } = yield options.req.getJSONBody(); - if (email === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'email'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (password === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'password'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - const signInResponse = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/dashboard/signin"), - { - email, - password, - } - ); - utils_1.send200Response(options.res, signInResponse); - return true; - }); -} -exports.default = signIn; diff --git a/lib/build/recipe/dashboard/api/signOut.d.ts b/lib/build/recipe/dashboard/api/signOut.d.ts deleted file mode 100644 index c9c5018d2..000000000 --- a/lib/build/recipe/dashboard/api/signOut.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export default function signOut(_: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/dashboard/api/signOut.js b/lib/build/recipe/dashboard/api/signOut.js deleted file mode 100644 index 929caddbe..000000000 --- a/lib/build/recipe/dashboard/api/signOut.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const querier_1 = require("../../../querier"); -const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -function signOut(_, options) { - var _a; - return __awaiter(this, void 0, void 0, function* () { - if (options.config.authMode === "api-key") { - utils_1.send200Response(options.res, { status: "OK" }); - } else { - const sessionIdFormAuthHeader = - (_a = options.req.getHeaderValue("authorization")) === null || _a === void 0 - ? void 0 - : _a.split(" ")[1]; - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - const sessionDeleteResponse = yield querier.sendDeleteRequest( - new normalisedURLPath_1.default("/recipe/dashboard/session"), - {}, - { sessionId: sessionIdFormAuthHeader } - ); - utils_1.send200Response(options.res, sessionDeleteResponse); - } - return true; - }); -} -exports.default = signOut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userDelete.d.ts b/lib/build/recipe/dashboard/api/userdetails/userDelete.d.ts deleted file mode 100644 index c184332e6..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userDelete.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type Response = { - status: "OK"; -}; -export declare const userDelete: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userDelete.js b/lib/build/recipe/dashboard/api/userdetails/userDelete.js deleted file mode 100644 index 0e53cb3d1..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userDelete.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userDelete = void 0; -const supertokens_1 = __importDefault(require("../../../../supertokens")); -const error_1 = __importDefault(require("../../../../error")); -const userDelete = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - yield supertokens_1.default.getInstanceOrThrowError().deleteUser({ - userId, - }); - return { - status: "OK", - }; - }); -exports.userDelete = userDelete; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.d.ts b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.d.ts deleted file mode 100644 index ba6da64a6..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIFunction } from "../../types"; -export declare const userEmailverifyGet: APIFunction; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.js b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.js deleted file mode 100644 index e835966b6..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyGet.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userEmailverifyGet = void 0; -const error_1 = __importDefault(require("../../../../error")); -const recipe_1 = __importDefault(require("../../../emailverification/recipe")); -const emailverification_1 = __importDefault(require("../../../emailverification")); -const userEmailverifyGet = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const req = options.req; - const userId = req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (e) { - return { - status: "FEATURE_NOT_ENABLED_ERROR", - }; - } - const response = yield emailverification_1.default.isEmailVerified(userId); - return { - status: "OK", - isVerified: response, - }; - }); -exports.userEmailverifyGet = userEmailverifyGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.d.ts deleted file mode 100644 index 055c3cb89..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type Response = { - status: "OK"; -}; -export declare const userEmailVerifyPut: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js deleted file mode 100644 index 6e8e0ed1d..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userEmailVerifyPut = void 0; -const error_1 = __importDefault(require("../../../../error")); -const emailverification_1 = __importDefault(require("../../../emailverification")); -const userEmailVerifyPut = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const verified = requestBody.verified; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (verified === undefined || typeof verified !== "boolean") { - throw new error_1.default({ - message: "Required parameter 'verified' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (verified) { - const tokenResponse = yield emailverification_1.default.createEmailVerificationToken(userId); - if (tokenResponse.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - return { - status: "OK", - }; - } - const verifyResponse = yield emailverification_1.default.verifyEmailUsingToken(tokenResponse.token); - if (verifyResponse.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { - // This should never happen because we consume the token immediately after creating it - throw new Error("Should not come here"); - } - } else { - yield emailverification_1.default.unverifyEmail(userId); - } - return { - status: "OK", - }; - }); -exports.userEmailVerifyPut = userEmailVerifyPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.d.ts b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.d.ts deleted file mode 100644 index 50b925c62..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type Response = { - status: "OK" | "EMAIL_ALREADY_VERIFIED_ERROR"; -}; -export declare const userEmailVerifyTokenPost: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.js b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.js deleted file mode 100644 index e24cd65eb..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userEmailVerifyTokenPost = void 0; -const error_1 = __importDefault(require("../../../../error")); -const emailverification_1 = __importDefault(require("../../../emailverification")); -const recipe_1 = __importDefault(require("../../../emailverification/recipe")); -const utils_1 = require("../../../emailverification/utils"); -const userEmailVerifyTokenPost = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let emailResponse = yield recipe_1.default.getInstanceOrThrowError().getEmailForUserId(userId, {}); - if (emailResponse.status !== "OK") { - throw new Error("Should never come here"); - } - let emailVerificationToken = yield emailverification_1.default.createEmailVerificationToken(userId); - if (emailVerificationToken.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } - let emailVerifyLink = utils_1.getEmailVerifyLink({ - appInfo: options.appInfo, - token: emailVerificationToken.token, - recipeId: recipe_1.default.RECIPE_ID, - }); - yield emailverification_1.default.sendEmail({ - type: "EMAIL_VERIFICATION", - user: { - id: userId, - email: emailResponse.email, - }, - emailVerifyLink, - }); - return { - status: "OK", - }; - }); -exports.userEmailVerifyTokenPost = userEmailVerifyTokenPost; diff --git a/lib/build/recipe/dashboard/api/userdetails/userGet.d.ts b/lib/build/recipe/dashboard/api/userdetails/userGet.d.ts deleted file mode 100644 index dd04c1ee3..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userGet.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIFunction } from "../../types"; -export declare const userGet: APIFunction; diff --git a/lib/build/recipe/dashboard/api/userdetails/userGet.js b/lib/build/recipe/dashboard/api/userdetails/userGet.js deleted file mode 100644 index e5d02516a..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userGet.js +++ /dev/null @@ -1,102 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userGet = void 0; -const error_1 = __importDefault(require("../../../../error")); -const utils_1 = require("../../utils"); -const recipe_1 = __importDefault(require("../../../usermetadata/recipe")); -const usermetadata_1 = __importDefault(require("../../../usermetadata")); -const userGet = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - const recipeId = options.req.getKeyValueFromQuery("recipeId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (recipeId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'recipeId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (!utils_1.isValidRecipeId(recipeId)) { - throw new error_1.default({ - message: "Invalid recipe id", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (!utils_1.isRecipeInitialised(recipeId)) { - return { - status: "RECIPE_NOT_INITIALISED", - }; - } - let user = (yield utils_1.getUserForRecipeId(userId, recipeId)).user; - if (user === undefined) { - return { - status: "NO_USER_FOUND_ERROR", - }; - } - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (_) { - user = Object.assign(Object.assign({}, user), { - firstName: "FEATURE_NOT_ENABLED", - lastName: "FEATURE_NOT_ENABLED", - }); - return { - status: "OK", - recipeId: recipeId, - user, - }; - } - const userMetaData = yield usermetadata_1.default.getUserMetadata(userId); - const { first_name, last_name } = userMetaData.metadata; - user = Object.assign(Object.assign({}, user), { - firstName: first_name === undefined ? "" : first_name, - lastName: last_name === undefined ? "" : last_name, - }); - return { - status: "OK", - recipeId: recipeId, - user, - }; - }); -exports.userGet = userGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.d.ts b/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.d.ts deleted file mode 100644 index f2ba7687d..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIFunction } from "../../types"; -export declare const userMetaDataGet: APIFunction; diff --git a/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.js b/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.js deleted file mode 100644 index dd7165022..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userMetadataGet.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userMetaDataGet = void 0; -const error_1 = __importDefault(require("../../../../error")); -const recipe_1 = __importDefault(require("../../../usermetadata/recipe")); -const usermetadata_1 = __importDefault(require("../../../usermetadata")); -const userMetaDataGet = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (e) { - return { - status: "FEATURE_NOT_ENABLED_ERROR", - }; - } - const metaDataResponse = usermetadata_1.default.getUserMetadata(userId); - return { - status: "OK", - data: (yield metaDataResponse).metadata, - }; - }); -exports.userMetaDataGet = userMetaDataGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.d.ts deleted file mode 100644 index 0bbbe8a65..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type Response = { - status: "OK"; -}; -export declare const userMetadataPut: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.js b/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.js deleted file mode 100644 index 2510e32fb..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.js +++ /dev/null @@ -1,97 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userMetadataPut = void 0; -const recipe_1 = __importDefault(require("../../../usermetadata/recipe")); -const usermetadata_1 = __importDefault(require("../../../usermetadata")); -const error_1 = __importDefault(require("../../../../error")); -const userMetadataPut = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const data = requestBody.data; - // This is to throw an error early in case the recipe has not been initialised - recipe_1.default.getInstanceOrThrowError(); - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (data === undefined || typeof data !== "string") { - throw new error_1.default({ - message: "Required parameter 'data' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - // Make sure that data is a valid JSON, this will throw - try { - let parsedData = JSON.parse(data); - if (typeof parsedData !== "object") { - throw new Error(); - } - if (Array.isArray(parsedData)) { - throw new Error(); - } - if (parsedData === null) { - throw new Error(); - } - } catch (e) { - throw new error_1.default({ - message: "'data' must be a valid JSON body", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - /** - * This API is meant to set the user metadata of a user. We delete the existing data - * before updating it because we want to make sure that shallow merging does not result - * in the data being incorrect - * - * For example if the old data is {test: "test", test2: "test2"} and the user wants to delete - * test2 from the data simply calling updateUserMetadata with {test: "test"} would not remove - * test2 because of shallow merging. - * - * Removing first ensures that the final data is exactly what the user wanted it to be - */ - yield usermetadata_1.default.clearUserMetadata(userId); - yield usermetadata_1.default.updateUserMetadata(userId, JSON.parse(data)); - return { - status: "OK", - }; - }); -exports.userMetadataPut = userMetadataPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.d.ts deleted file mode 100644 index d9381e6b6..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type Response = - | { - status: "OK"; - } - | { - status: "INVALID_PASSWORD_ERROR"; - error: string; - }; -export declare const userPasswordPut: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js deleted file mode 100644 index c23cb5299..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js +++ /dev/null @@ -1,131 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userPasswordPut = void 0; -const error_1 = __importDefault(require("../../../../error")); -const recipe_1 = __importDefault(require("../../../emailpassword/recipe")); -const emailpassword_1 = __importDefault(require("../../../emailpassword")); -const recipe_2 = __importDefault(require("../../../thirdpartyemailpassword/recipe")); -const thirdpartyemailpassword_1 = __importDefault(require("../../../thirdpartyemailpassword")); -const constants_1 = require("../../../emailpassword/constants"); -const userPasswordPut = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const newPassword = requestBody.newPassword; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (newPassword === undefined || typeof newPassword !== "string") { - throw new error_1.default({ - message: "Required parameter 'newPassword' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let recipeToUse; - try { - recipe_1.default.getInstanceOrThrowError(); - recipeToUse = "emailpassword"; - } catch (_) {} - if (recipeToUse === undefined) { - try { - recipe_2.default.getInstanceOrThrowError(); - recipeToUse = "thirdpartyemailpassword"; - } catch (_) {} - } - if (recipeToUse === undefined) { - // This means that neither emailpassword or thirdpartyemailpassword is initialised - throw new Error("Should never come here"); - } - if (recipeToUse === "emailpassword") { - let passwordFormFields = recipe_1.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_PASSWORD_ID); - let passwordValidationError = yield passwordFormFields[0].validate(newPassword); - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - const passwordResetToken = yield emailpassword_1.default.createResetPasswordToken(userId); - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - const passwordResetResponse = yield emailpassword_1.default.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - return { - status: "OK", - }; - } - let passwordFormFields = recipe_2.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_PASSWORD_ID); - let passwordValidationError = yield passwordFormFields[0].validate(newPassword); - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - const passwordResetToken = yield thirdpartyemailpassword_1.default.createResetPasswordToken(userId); - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - const passwordResetResponse = yield thirdpartyemailpassword_1.default.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - return { - status: "OK", - }; - }); -exports.userPasswordPut = userPasswordPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts deleted file mode 100644 index ba250bb3d..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type Response = - | { - status: "OK"; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | { - status: "INVALID_EMAIL_ERROR"; - error: string; - } - | { - status: "PHONE_ALREADY_EXISTS_ERROR"; - } - | { - status: "INVALID_PHONE_ERROR"; - error: string; - }; -export declare const userPut: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPut.js b/lib/build/recipe/dashboard/api/userdetails/userPut.js deleted file mode 100644 index 85907607a..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userPut.js +++ /dev/null @@ -1,365 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userPut = void 0; -const error_1 = __importDefault(require("../../../../error")); -const recipe_1 = __importDefault(require("../../../emailpassword/recipe")); -const recipe_2 = __importDefault(require("../../../thirdpartyemailpassword/recipe")); -const recipe_3 = __importDefault(require("../../../passwordless/recipe")); -const recipe_4 = __importDefault(require("../../../thirdpartypasswordless/recipe")); -const emailpassword_1 = __importDefault(require("../../../emailpassword")); -const passwordless_1 = __importDefault(require("../../../passwordless")); -const thirdpartyemailpassword_1 = __importDefault(require("../../../thirdpartyemailpassword")); -const thirdpartypasswordless_1 = __importDefault(require("../../../thirdpartypasswordless")); -const utils_1 = require("../../utils"); -const recipe_5 = __importDefault(require("../../../usermetadata/recipe")); -const usermetadata_1 = __importDefault(require("../../../usermetadata")); -const constants_1 = require("../../../emailpassword/constants"); -const utils_2 = require("../../../passwordless/utils"); -const updateEmailForRecipeId = (recipeId, userId, email) => - __awaiter(void 0, void 0, void 0, function* () { - if (recipeId === "emailpassword") { - let emailFormFields = recipe_1.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_EMAIL_ID); - let validationError = yield emailFormFields[0].validate(email); - if (validationError !== undefined) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const emailUpdateResponse = yield emailpassword_1.default.updateEmailOrPassword({ - userId, - email, - }); - if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - return { - status: "OK", - }; - } - if (recipeId === "thirdpartyemailpassword") { - let emailFormFields = recipe_2.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_EMAIL_ID); - let validationError = yield emailFormFields[0].validate(email); - if (validationError !== undefined) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const emailUpdateResponse = yield thirdpartyemailpassword_1.default.updateEmailOrPassword({ - userId, - email, - }); - if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - return { - status: "OK", - }; - } - if (recipeId === "passwordless") { - let isValidEmail = true; - let validationError = ""; - const passwordlessConfig = recipe_3.default.getInstanceOrThrowError().config; - if (passwordlessConfig.contactMethod === "PHONE") { - const validationResult = yield utils_2.defaultValidateEmail(email); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validateEmailAddress(email); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } - if (!isValidEmail) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const updateResult = yield passwordless_1.default.updateUser({ - userId, - email, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - return { - status: "OK", - }; - } - if (recipeId === "thirdpartypasswordless") { - let isValidEmail = true; - let validationError = ""; - const passwordlessConfig = recipe_4.default.getInstanceOrThrowError().passwordlessRecipe.config; - if (passwordlessConfig.contactMethod === "PHONE") { - const validationResult = yield utils_2.defaultValidateEmail(email); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validateEmailAddress(email); - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } - if (!isValidEmail) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - const updateResult = yield thirdpartypasswordless_1.default.updatePasswordlessUser({ - userId, - email, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - return { - status: "OK", - }; - } - /** - * If it comes here then the user is a third party user in which case the UI should not have allowed this - */ - throw new Error("Should never come here"); - }); -const updatePhoneForRecipeId = (recipeId, userId, phone) => - __awaiter(void 0, void 0, void 0, function* () { - if (recipeId === "passwordless") { - let isValidPhone = true; - let validationError = ""; - const passwordlessConfig = recipe_3.default.getInstanceOrThrowError().config; - if (passwordlessConfig.contactMethod === "EMAIL") { - const validationResult = yield utils_2.defaultValidatePhoneNumber(phone); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validatePhoneNumber(phone); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } - if (!isValidPhone) { - return { - status: "INVALID_PHONE_ERROR", - error: validationError, - }; - } - const updateResult = yield passwordless_1.default.updateUser({ - userId, - phoneNumber: phone, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { - return { - status: "PHONE_ALREADY_EXISTS_ERROR", - }; - } - return { - status: "OK", - }; - } - if (recipeId === "thirdpartypasswordless") { - let isValidPhone = true; - let validationError = ""; - const passwordlessConfig = recipe_4.default.getInstanceOrThrowError().passwordlessRecipe.config; - if (passwordlessConfig.contactMethod === "EMAIL") { - const validationResult = yield utils_2.defaultValidatePhoneNumber(phone); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } else { - const validationResult = yield passwordlessConfig.validatePhoneNumber(phone); - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } - if (!isValidPhone) { - return { - status: "INVALID_PHONE_ERROR", - error: validationError, - }; - } - const updateResult = yield thirdpartypasswordless_1.default.updatePasswordlessUser({ - userId, - phoneNumber: phone, - }); - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { - return { - status: "PHONE_ALREADY_EXISTS_ERROR", - }; - } - return { - status: "OK", - }; - } - /** - * If it comes here then the user is a not a passwordless user in which case the UI should not have allowed this - */ - throw new Error("Should never come here"); - }); -const userPut = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const userId = requestBody.userId; - const recipeId = requestBody.recipeId; - const firstName = requestBody.firstName; - const lastName = requestBody.lastName; - const email = requestBody.email; - const phone = requestBody.phone; - if (userId === undefined || typeof userId !== "string") { - throw new error_1.default({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (recipeId === undefined || typeof recipeId !== "string") { - throw new error_1.default({ - message: "Required parameter 'recipeId' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (!utils_1.isValidRecipeId(recipeId)) { - throw new error_1.default({ - message: "Invalid recipe id", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (firstName === undefined || typeof firstName !== "string") { - throw new error_1.default({ - message: "Required parameter 'firstName' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (lastName === undefined || typeof lastName !== "string") { - throw new error_1.default({ - message: "Required parameter 'lastName' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (email === undefined || typeof email !== "string") { - throw new error_1.default({ - message: "Required parameter 'email' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - if (phone === undefined || typeof phone !== "string") { - throw new error_1.default({ - message: "Required parameter 'phone' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let userResponse = yield utils_1.getUserForRecipeId(userId, recipeId); - if (userResponse.user === undefined || userResponse.recipe === undefined) { - throw new Error("Should never come here"); - } - if (firstName.trim() !== "" || lastName.trim() !== "") { - let isRecipeInitialised = false; - try { - recipe_5.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) { - // no op - } - if (isRecipeInitialised) { - let metaDataUpdate = {}; - if (firstName.trim() !== "") { - metaDataUpdate["first_name"] = firstName.trim(); - } - if (lastName.trim() !== "") { - metaDataUpdate["last_name"] = lastName.trim(); - } - yield usermetadata_1.default.updateUserMetadata(userId, metaDataUpdate); - } - } - if (email.trim() !== "") { - const emailUpdateResponse = yield updateEmailForRecipeId(userResponse.recipe, userId, email.trim()); - if (emailUpdateResponse.status !== "OK") { - return emailUpdateResponse; - } - } - if (phone.trim() !== "") { - const phoneUpdateResponse = yield updatePhoneForRecipeId(userResponse.recipe, userId, phone.trim()); - if (phoneUpdateResponse.status !== "OK") { - return phoneUpdateResponse; - } - } - return { - status: "OK", - }; - }); -exports.userPut = userPut; diff --git a/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.d.ts b/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.d.ts deleted file mode 100644 index 34a18f080..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIFunction } from "../../types"; -export declare const userSessionsGet: APIFunction; diff --git a/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.js b/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.js deleted file mode 100644 index 6cae91b07..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userSessionsGet.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userSessionsGet = void 0; -const error_1 = __importDefault(require("../../../../error")); -const session_1 = __importDefault(require("../../../session")); -const userSessionsGet = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const userId = options.req.getKeyValueFromQuery("userId"); - if (userId === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'userId'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - const response = yield session_1.default.getAllSessionHandlesForUser(userId); - let sessions = []; - let sessionInfoPromises = []; - for (let i = 0; i < response.length; i++) { - sessionInfoPromises.push( - new Promise((res, rej) => - __awaiter(void 0, void 0, void 0, function* () { - try { - const sessionResponse = yield session_1.default.getSessionInformation(response[i]); - if (sessionResponse !== undefined) { - sessions[i] = sessionResponse; - } - res(); - } catch (e) { - rej(e); - } - }) - ) - ); - } - yield Promise.all(sessionInfoPromises); - return { - status: "OK", - sessions, - }; - }); -exports.userSessionsGet = userSessionsGet; diff --git a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.d.ts b/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.d.ts deleted file mode 100644 index 2a5a7eeb7..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../../types"; -declare type Response = { - status: "OK"; -}; -export declare const userSessionsPost: (_: APIInterface, options: APIOptions) => Promise; -export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.js b/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.js deleted file mode 100644 index a65859ca9..000000000 --- a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.userSessionsPost = void 0; -const error_1 = __importDefault(require("../../../../error")); -const session_1 = __importDefault(require("../../../session")); -const userSessionsPost = (_, options) => - __awaiter(void 0, void 0, void 0, function* () { - const requestBody = yield options.req.getJSONBody(); - const sessionHandles = requestBody.sessionHandles; - if (sessionHandles === undefined || !Array.isArray(sessionHandles)) { - throw new error_1.default({ - message: "Required parameter 'sessionHandles' is missing or has an invalid type", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - yield session_1.default.revokeMultipleSessions(sessionHandles); - return { - status: "OK", - }; - }); -exports.userSessionsPost = userSessionsPost; diff --git a/lib/build/recipe/dashboard/api/usersCountGet.d.ts b/lib/build/recipe/dashboard/api/usersCountGet.d.ts deleted file mode 100644 index 9291de948..000000000 --- a/lib/build/recipe/dashboard/api/usersCountGet.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export declare type Response = { - status: "OK"; - count: number; -}; -export default function usersCountGet(_: APIInterface, __: APIOptions): Promise; diff --git a/lib/build/recipe/dashboard/api/usersCountGet.js b/lib/build/recipe/dashboard/api/usersCountGet.js deleted file mode 100644 index 59b8f8f74..000000000 --- a/lib/build/recipe/dashboard/api/usersCountGet.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const supertokens_1 = __importDefault(require("../../../supertokens")); -function usersCountGet(_, __) { - return __awaiter(this, void 0, void 0, function* () { - const count = yield supertokens_1.default.getInstanceOrThrowError().getUserCount(); - return { - status: "OK", - count, - }; - }); -} -exports.default = usersCountGet; diff --git a/lib/build/recipe/dashboard/api/usersGet.d.ts b/lib/build/recipe/dashboard/api/usersGet.d.ts deleted file mode 100644 index 8d445cb5b..000000000 --- a/lib/build/recipe/dashboard/api/usersGet.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export declare type Response = { - status: "OK"; - nextPaginationToken?: string; - users: { - recipeId: string; - user: { - id: string; - timeJoined: number; - firstName?: string; - lastName?: string; - } & ( - | { - email: string; - } - | { - email: string; - thirdParty: { - id: string; - userId: string; - }; - } - | { - email?: string; - phoneNumber?: string; - } - ); - }[]; -}; -export default function usersGet(_: APIInterface, options: APIOptions): Promise; -export declare function getSearchParamsFromURL( - path: string -): { - [key: string]: string; -}; diff --git a/lib/build/recipe/dashboard/api/usersGet.js b/lib/build/recipe/dashboard/api/usersGet.js deleted file mode 100644 index 3c1708c6f..000000000 --- a/lib/build/recipe/dashboard/api/usersGet.js +++ /dev/null @@ -1,159 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getSearchParamsFromURL = void 0; -const error_1 = __importDefault(require("../../../error")); -const supertokens_1 = __importDefault(require("../../../supertokens")); -const recipe_1 = __importDefault(require("../../usermetadata/recipe")); -const usermetadata_1 = __importDefault(require("../../usermetadata")); -function usersGet(_, options) { - return __awaiter(this, void 0, void 0, function* () { - const req = options.req; - const limit = options.req.getKeyValueFromQuery("limit"); - if (limit === undefined) { - throw new error_1.default({ - message: "Missing required parameter 'limit'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let timeJoinedOrder = req.getKeyValueFromQuery("timeJoinedOrder"); - if (timeJoinedOrder === undefined) { - timeJoinedOrder = "DESC"; - } - if (timeJoinedOrder !== "ASC" && timeJoinedOrder !== "DESC") { - throw new error_1.default({ - message: "Invalid value recieved for 'timeJoinedOrder'", - type: error_1.default.BAD_INPUT_ERROR, - }); - } - let paginationToken = options.req.getKeyValueFromQuery("paginationToken"); - const query = getSearchParamsFromURL(options.req.getOriginalURL()); - let usersResponse = yield supertokens_1.default.getInstanceOrThrowError().getUsers({ - query, - timeJoinedOrder: timeJoinedOrder, - limit: parseInt(limit), - paginationToken, - }); - // If the UserMetaData recipe has been initialised, fetch first and last name - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (e) { - // Recipe has not been initialised, return without first name and last name - return { - status: "OK", - users: usersResponse.users, - nextPaginationToken: usersResponse.nextPaginationToken, - }; - } - let updatedUsersArray = []; - let metaDataFetchPromises = []; - for (let i = 0; i < usersResponse.users.length; i++) { - const userObj = usersResponse.users[i]; - metaDataFetchPromises.push( - () => - new Promise((resolve, reject) => - __awaiter(this, void 0, void 0, function* () { - try { - const userMetaDataResponse = yield usermetadata_1.default.getUserMetadata( - userObj.user.id - ); - const { first_name, last_name } = userMetaDataResponse.metadata; - updatedUsersArray[i] = { - recipeId: userObj.recipeId, - user: Object.assign(Object.assign({}, userObj.user), { - firstName: first_name, - lastName: last_name, - }), - }; - resolve(true); - } catch (e) { - // Something went wrong when fetching user meta data - reject(e); - } - }) - ) - ); - } - let promiseArrayStartPosition = 0; - let batchSize = 5; - while (promiseArrayStartPosition < metaDataFetchPromises.length) { - /** - * We want to query only 5 in parallel at a time - * - * First we check if the the array has enough elements to iterate - * promiseArrayStartPosition + 4 (5 elements including current) - */ - let promiseArrayEndPosition = promiseArrayStartPosition + (batchSize - 1); - // If the end position is higher than the arrays length, we need to adjust it - if (promiseArrayEndPosition >= metaDataFetchPromises.length) { - /** - * For example if the array has 7 elements [A, B, C, D, E, F, G], when you run - * the second batch [startPosition = 5], this will result in promiseArrayEndPosition - * to be equal to 6 [5 + ((7 - 1) - 5)] and will then iterate over indexes [5] and [6] - */ - promiseArrayEndPosition = - promiseArrayStartPosition + (metaDataFetchPromises.length - 1 - promiseArrayStartPosition); - } - let promisesToCall = []; - for (let j = promiseArrayStartPosition; j <= promiseArrayEndPosition; j++) { - promisesToCall.push(metaDataFetchPromises[j]); - } - yield Promise.all(promisesToCall.map((p) => p())); - promiseArrayStartPosition += batchSize; - } - usersResponse = Object.assign(Object.assign({}, usersResponse), { users: updatedUsersArray }); - return { - status: "OK", - users: usersResponse.users, - nextPaginationToken: usersResponse.nextPaginationToken, - }; - }); -} -exports.default = usersGet; -function getSearchParamsFromURL(path) { - const URLObject = new URL("https://exmaple.com" + path); - const params = new URLSearchParams(URLObject.search); - const searchQuery = {}; - for (const [key, value] of params) { - if (!["limit", "timeJoinedOrder", "paginationToken"].includes(key)) { - searchQuery[key] = value; - } - } - return searchQuery; -} -exports.getSearchParamsFromURL = getSearchParamsFromURL; diff --git a/lib/build/recipe/dashboard/api/validateKey.d.ts b/lib/build/recipe/dashboard/api/validateKey.d.ts deleted file mode 100644 index 39fa8c2a0..000000000 --- a/lib/build/recipe/dashboard/api/validateKey.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export default function validateKey(_: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/dashboard/api/validateKey.js b/lib/build/recipe/dashboard/api/validateKey.js deleted file mode 100644 index 482e51bac..000000000 --- a/lib/build/recipe/dashboard/api/validateKey.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("../utils"); -function validateKey(_, options) { - return __awaiter(this, void 0, void 0, function* () { - const input = { - req: options.req, - config: options.config, - userContext: utils_1.makeDefaultUserContextFromAPI(options.req), - }; - if (yield utils_2.validateApiKey(input)) { - options.res.sendJSONResponse({ - status: "OK", - }); - } else { - utils_2.sendUnauthorisedAccess(options.res); - } - return true; - }); -} -exports.default = validateKey; diff --git a/lib/build/recipe/dashboard/constants.d.ts b/lib/build/recipe/dashboard/constants.d.ts deleted file mode 100644 index 7b0c33787..000000000 --- a/lib/build/recipe/dashboard/constants.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -export declare const DASHBOARD_API = "/dashboard"; -export declare const SIGN_IN_API = "/api/signin"; -export declare const SIGN_OUT_API = "/api/signout"; -export declare const VALIDATE_KEY_API = "/api/key/validate"; -export declare const USERS_LIST_GET_API = "/api/users"; -export declare const USERS_COUNT_API = "/api/users/count"; -export declare const USER_API = "/api/user"; -export declare const USER_EMAIL_VERIFY_API = "/api/user/email/verify"; -export declare const USER_METADATA_API = "/api/user/metadata"; -export declare const USER_SESSIONS_API = "/api/user/sessions"; -export declare const USER_PASSWORD_API = "/api/user/password"; -export declare const USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token"; -export declare const SEARCH_TAGS_API = "/api/search/tags"; -export declare const DASHBOARD_ANALYTICS_API = "/api/analytics"; diff --git a/lib/build/recipe/dashboard/constants.js b/lib/build/recipe/dashboard/constants.js deleted file mode 100644 index 9cf1620dc..000000000 --- a/lib/build/recipe/dashboard/constants.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DASHBOARD_ANALYTICS_API = exports.SEARCH_TAGS_API = exports.USER_EMAIL_VERIFY_TOKEN_API = exports.USER_PASSWORD_API = exports.USER_SESSIONS_API = exports.USER_METADATA_API = exports.USER_EMAIL_VERIFY_API = exports.USER_API = exports.USERS_COUNT_API = exports.USERS_LIST_GET_API = exports.VALIDATE_KEY_API = exports.SIGN_OUT_API = exports.SIGN_IN_API = exports.DASHBOARD_API = void 0; -exports.DASHBOARD_API = "/dashboard"; -exports.SIGN_IN_API = "/api/signin"; -exports.SIGN_OUT_API = "/api/signout"; -exports.VALIDATE_KEY_API = "/api/key/validate"; -exports.USERS_LIST_GET_API = "/api/users"; -exports.USERS_COUNT_API = "/api/users/count"; -exports.USER_API = "/api/user"; -exports.USER_EMAIL_VERIFY_API = "/api/user/email/verify"; -exports.USER_METADATA_API = "/api/user/metadata"; -exports.USER_SESSIONS_API = "/api/user/sessions"; -exports.USER_PASSWORD_API = "/api/user/password"; -exports.USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token"; -exports.SEARCH_TAGS_API = "/api/search/tags"; -exports.DASHBOARD_ANALYTICS_API = "/api/analytics"; diff --git a/lib/build/recipe/dashboard/index.d.ts b/lib/build/recipe/dashboard/index.d.ts deleted file mode 100644 index 3fc17a34f..000000000 --- a/lib/build/recipe/dashboard/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import { RecipeInterface, APIOptions, APIInterface } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; -} -export declare let init: typeof Recipe.init; -export type { RecipeInterface, APIOptions, APIInterface }; diff --git a/lib/build/recipe/dashboard/index.js b/lib/build/recipe/dashboard/index.js deleted file mode 100644 index 7ad60228f..000000000 --- a/lib/build/recipe/dashboard/index.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.init = void 0; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipe_1 = __importDefault(require("./recipe")); -class Wrapper {} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -exports.init = Wrapper.init; diff --git a/lib/build/recipe/dashboard/recipe.d.ts b/lib/build/recipe/dashboard/recipe.d.ts deleted file mode 100644 index d393f7047..000000000 --- a/lib/build/recipe/dashboard/recipe.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import error from "../../error"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); - static getInstanceOrThrowError(): Recipe; - static init(config?: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - __: NormalisedURLPath, - ___: HTTPMethod - ) => Promise; - handleError: (err: error, _: BaseRequest, __: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is error; - returnAPIIdIfCanHandleRequest: (path: NormalisedURLPath, method: HTTPMethod) => string | undefined; -} diff --git a/lib/build/recipe/dashboard/recipe.js b/lib/build/recipe/dashboard/recipe.js deleted file mode 100644 index 3b24127e7..000000000 --- a/lib/build/recipe/dashboard/recipe.js +++ /dev/null @@ -1,385 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const utils_1 = require("./utils"); -const constants_1 = require("./constants"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const dashboard_1 = __importDefault(require("./api/dashboard")); -const error_1 = __importDefault(require("../../error")); -const validateKey_1 = __importDefault(require("./api/validateKey")); -const apiKeyProtector_1 = __importDefault(require("./api/apiKeyProtector")); -const usersGet_1 = __importDefault(require("./api/usersGet")); -const usersCountGet_1 = __importDefault(require("./api/usersCountGet")); -const userGet_1 = require("./api/userdetails/userGet"); -const userEmailVerifyGet_1 = require("./api/userdetails/userEmailVerifyGet"); -const userMetadataGet_1 = require("./api/userdetails/userMetadataGet"); -const userSessionsGet_1 = require("./api/userdetails/userSessionsGet"); -const userDelete_1 = require("./api/userdetails/userDelete"); -const userEmailVerifyPut_1 = require("./api/userdetails/userEmailVerifyPut"); -const userMetadataPut_1 = require("./api/userdetails/userMetadataPut"); -const userPasswordPut_1 = require("./api/userdetails/userPasswordPut"); -const userPut_1 = require("./api/userdetails/userPut"); -const userEmailVerifyTokenPost_1 = require("./api/userdetails/userEmailVerifyTokenPost"); -const userSessionsPost_1 = require("./api/userdetails/userSessionsPost"); -const signIn_1 = __importDefault(require("./api/signIn")); -const signOut_1 = __importDefault(require("./api/signOut")); -const tagsGet_1 = require("./api/search/tagsGet"); -const analytics_1 = __importDefault(require("./api/analytics")); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config) { - super(recipeId, appInfo); - // abstract instance functions below............... - this.getAPIsHandled = () => { - /** - * Normally this array is used by the SDK to decide whether or not the recipe - * handles a specific API path and method and then returns the ID. - * - * For the dashboard recipe this logic is fully custom and handled inside the - * `returnAPIIdIfCanHandleRequest` method of this class. - * - * For most frameworks this array is redundant because the `returnAPIIdIfCanHandleRequest` is used. - * But for frameworks such as Hapi that require all APIs to be declared up front, this array is used - * to make sure that the framework does not return a 404 - */ - return [ - { - id: constants_1.DASHBOARD_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.DASHBOARD_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.SIGN_IN_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.SIGN_IN_API) - ), - disabled: false, - method: "post", - }, - { - id: constants_1.VALIDATE_KEY_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.VALIDATE_KEY_API) - ), - disabled: false, - method: "post", - }, - { - id: constants_1.SIGN_OUT_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.SIGN_OUT_API) - ), - disabled: false, - method: "post", - }, - { - id: constants_1.USERS_LIST_GET_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USERS_LIST_GET_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.USERS_COUNT_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USERS_COUNT_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.USER_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.USER_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_API) - ), - disabled: false, - method: "post", - }, - { - id: constants_1.USER_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_API) - ), - disabled: false, - method: "delete", - }, - { - id: constants_1.USER_EMAIL_VERIFY_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_EMAIL_VERIFY_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.USER_EMAIL_VERIFY_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_EMAIL_VERIFY_API) - ), - disabled: false, - method: "put", - }, - { - id: constants_1.USER_METADATA_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_METADATA_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.USER_METADATA_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_METADATA_API) - ), - disabled: false, - method: "put", - }, - { - id: constants_1.USER_SESSIONS_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_SESSIONS_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.USER_SESSIONS_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_SESSIONS_API) - ), - disabled: false, - method: "post", - }, - { - id: constants_1.USER_PASSWORD_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_PASSWORD_API) - ), - disabled: false, - method: "put", - }, - { - id: constants_1.USER_EMAIL_VERIFY_TOKEN_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.USER_EMAIL_VERIFY_TOKEN_API) - ), - disabled: false, - method: "post", - }, - { - id: constants_1.SEARCH_TAGS_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.SEARCH_TAGS_API) - ), - disabled: false, - method: "get", - }, - { - id: constants_1.DASHBOARD_ANALYTICS_API, - pathWithoutApiBasePath: new normalisedURLPath_1.default( - utils_1.getApiPathWithDashboardBase(constants_1.DASHBOARD_ANALYTICS_API) - ), - disabled: false, - method: "post", - }, - ]; - }; - this.handleAPIRequest = (id, req, res, __, ___) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - isInServerlessEnv: this.isInServerlessEnv, - appInfo: this.getAppInfo(), - }; - // For these APIs we dont need API key validation - if (id === constants_1.DASHBOARD_API) { - return yield dashboard_1.default(this.apiImpl, options); - } - if (id === constants_1.SIGN_IN_API) { - return yield signIn_1.default(this.apiImpl, options); - } - if (id === constants_1.VALIDATE_KEY_API) { - return yield validateKey_1.default(this.apiImpl, options); - } - // Do API key validation for the remaining APIs - let apiFunction; - if (id === constants_1.USERS_LIST_GET_API) { - apiFunction = usersGet_1.default; - } else if (id === constants_1.USERS_COUNT_API) { - apiFunction = usersCountGet_1.default; - } else if (id === constants_1.USER_API) { - if (req.getMethod() === "get") { - apiFunction = userGet_1.userGet; - } - if (req.getMethod() === "delete") { - apiFunction = userDelete_1.userDelete; - } - if (req.getMethod() === "put") { - apiFunction = userPut_1.userPut; - } - } else if (id === constants_1.USER_EMAIL_VERIFY_API) { - if (req.getMethod() === "get") { - apiFunction = userEmailVerifyGet_1.userEmailverifyGet; - } - if (req.getMethod() === "put") { - apiFunction = userEmailVerifyPut_1.userEmailVerifyPut; - } - } else if (id === constants_1.USER_METADATA_API) { - if (req.getMethod() === "get") { - apiFunction = userMetadataGet_1.userMetaDataGet; - } - if (req.getMethod() === "put") { - apiFunction = userMetadataPut_1.userMetadataPut; - } - } else if (id === constants_1.USER_SESSIONS_API) { - if (req.getMethod() === "get") { - apiFunction = userSessionsGet_1.userSessionsGet; - } - if (req.getMethod() === "post") { - apiFunction = userSessionsPost_1.userSessionsPost; - } - } else if (id === constants_1.USER_PASSWORD_API) { - apiFunction = userPasswordPut_1.userPasswordPut; - } else if (id === constants_1.USER_EMAIL_VERIFY_TOKEN_API) { - apiFunction = userEmailVerifyTokenPost_1.userEmailVerifyTokenPost; - } else if (id === constants_1.SEARCH_TAGS_API) { - apiFunction = tagsGet_1.getSearchTags; - } else if (id === constants_1.SIGN_OUT_API) { - apiFunction = signOut_1.default; - } else if (id === constants_1.DASHBOARD_ANALYTICS_API && req.getMethod() === "post") { - apiFunction = analytics_1.default; - } - // If the id doesnt match any APIs return false - if (apiFunction === undefined) { - return false; - } - return yield apiKeyProtector_1.default(this.apiImpl, options, apiFunction); - }); - this.handleError = (err, _, __) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); - this.getAllCORSHeaders = () => { - return []; - }; - this.isErrorFromThisRecipe = (err) => { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - this.returnAPIIdIfCanHandleRequest = (path, method) => { - const dashboardBundlePath = this.getAppInfo().apiBasePath.appendPath( - new normalisedURLPath_1.default(constants_1.DASHBOARD_API) - ); - if (utils_1.isApiPath(path, this.getAppInfo())) { - return utils_1.getApiIdIfMatched(path, method); - } - if (path.startsWith(dashboardBundlePath)) { - return constants_1.DASHBOARD_API; - } - return undefined; - }; - this.config = utils_1.validateAndNormaliseUserInput(config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new supertokens_js_override_1.default(recipeImplementation_1.default()); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("Dashboard recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "dashboard"; diff --git a/lib/build/recipe/dashboard/recipeImplementation.d.ts b/lib/build/recipe/dashboard/recipeImplementation.d.ts deleted file mode 100644 index 881866f94..000000000 --- a/lib/build/recipe/dashboard/recipeImplementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "./types"; -export default function getRecipeImplementation(): RecipeInterface; diff --git a/lib/build/recipe/dashboard/recipeImplementation.js b/lib/build/recipe/dashboard/recipeImplementation.js deleted file mode 100644 index a3e948462..000000000 --- a/lib/build/recipe/dashboard/recipeImplementation.js +++ /dev/null @@ -1,88 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const querier_1 = require("../../querier"); -const version_1 = require("../../version"); -const utils_1 = require("./utils"); -function getRecipeImplementation() { - return { - getDashboardBundleLocation: function () { - return __awaiter(this, void 0, void 0, function* () { - return `https://cdn.jsdelivr.net/gh/supertokens/dashboard@v${version_1.dashboardVersion}/build/`; - }); - }, - shouldAllowAccess: function (input) { - var _a; - return __awaiter(this, void 0, void 0, function* () { - // For cases where we're not using the API key, the JWT is being used; we allow their access by default - if (!input.config.apiKey) { - // make the check for the API endpoint here with querier - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - const authHeaderValue = - (_a = input.req.getHeaderValue("authorization")) === null || _a === void 0 - ? void 0 - : _a.split(" ")[1]; - const sessionVerificationResponse = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/dashboard/session/verify"), - { - sessionId: authHeaderValue, - } - ); - return sessionVerificationResponse.status === "OK"; - } - return yield utils_1.validateApiKey(input); - }); - }, - }; -} -exports.default = getRecipeImplementation; diff --git a/lib/build/recipe/dashboard/types.d.ts b/lib/build/recipe/dashboard/types.d.ts deleted file mode 100644 index 0168613aa..000000000 --- a/lib/build/recipe/dashboard/types.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; -export declare type TypeInput = { - apiKey?: string; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - apiKey?: string; - authMode: AuthMode; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type RecipeInterface = { - getDashboardBundleLocation(input: { userContext: any }): Promise; - shouldAllowAccess(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }): Promise; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - req: BaseRequest; - res: BaseResponse; - isInServerlessEnv: boolean; - appInfo: NormalisedAppinfo; -}; -export declare type APIInterface = { - dashboardGET: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); -}; -export declare type APIFunction = (apiImplementation: APIInterface, options: APIOptions) => Promise; -export declare type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; -export declare type AuthMode = "api-key" | "email-password"; -declare type CommonUserInformation = { - id: string; - timeJoined: number; - firstName: string; - lastName: string; -}; -export declare type EmailPasswordUser = CommonUserInformation & { - email: string; -}; -export declare type ThirdPartyUser = CommonUserInformation & { - email: string; - thirdParty: { - id: string; - userId: string; - }; -}; -export declare type PasswordlessUser = CommonUserInformation & { - email?: string; - phone?: string; -}; -export {}; diff --git a/lib/build/recipe/dashboard/types.js b/lib/build/recipe/dashboard/types.js deleted file mode 100644 index a7bb3574b..000000000 --- a/lib/build/recipe/dashboard/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/dashboard/utils.d.ts b/lib/build/recipe/dashboard/utils.d.ts deleted file mode 100644 index 6d9e1b78f..000000000 --- a/lib/build/recipe/dashboard/utils.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { HTTPMethod, NormalisedAppinfo } from "../../types"; -import { - EmailPasswordUser, - PasswordlessUser, - RecipeIdForUser, - ThirdPartyUser, - TypeInput, - TypeNormalisedInput, -} from "./types"; -export declare function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput; -export declare function isApiPath(path: NormalisedURLPath, appInfo: NormalisedAppinfo): boolean; -export declare function getApiIdIfMatched(path: NormalisedURLPath, method: HTTPMethod): string | undefined; -export declare function sendUnauthorisedAccess(res: BaseResponse): void; -export declare function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser; -export declare function getUserForRecipeId( - userId: string, - recipeId: string -): Promise<{ - user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined; - recipe: - | "emailpassword" - | "thirdparty" - | "passwordless" - | "thirdpartyemailpassword" - | "thirdpartypasswordless" - | undefined; -}>; -export declare function isRecipeInitialised(recipeId: RecipeIdForUser): boolean; -export declare function validateApiKey(input: { - req: BaseRequest; - config: TypeNormalisedInput; - userContext: any; -}): Promise; -export declare function getApiPathWithDashboardBase(path: string): string; diff --git a/lib/build/recipe/dashboard/utils.js b/lib/build/recipe/dashboard/utils.js deleted file mode 100644 index e98193781..000000000 --- a/lib/build/recipe/dashboard/utils.js +++ /dev/null @@ -1,309 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getApiPathWithDashboardBase = exports.validateApiKey = exports.isRecipeInitialised = exports.getUserForRecipeId = exports.isValidRecipeId = exports.sendUnauthorisedAccess = exports.getApiIdIfMatched = exports.isApiPath = exports.validateAndNormaliseUserInput = void 0; -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const utils_1 = require("../../utils"); -const constants_1 = require("./constants"); -const recipe_1 = __importDefault(require("../emailpassword/recipe")); -const recipe_2 = __importDefault(require("../thirdparty/recipe")); -const recipe_3 = __importDefault(require("../passwordless/recipe")); -const emailpassword_1 = __importDefault(require("../emailpassword")); -const thirdparty_1 = __importDefault(require("../thirdparty")); -const passwordless_1 = __importDefault(require("../passwordless")); -const thirdpartyemailpassword_1 = __importDefault(require("../thirdpartyemailpassword")); -const recipe_4 = __importDefault(require("../thirdpartyemailpassword/recipe")); -const thirdpartypasswordless_1 = __importDefault(require("../thirdpartypasswordless")); -const recipe_5 = __importDefault(require("../thirdpartypasswordless/recipe")); -function validateAndNormaliseUserInput(config) { - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === undefined ? {} : config.override - ); - return { - override, - authMode: config !== undefined && config.apiKey ? "api-key" : "email-password", - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; -function isApiPath(path, appInfo) { - const dashboardRecipeBasePath = appInfo.apiBasePath.appendPath( - new normalisedURLPath_1.default(constants_1.DASHBOARD_API) - ); - if (!path.startsWith(dashboardRecipeBasePath)) { - return false; - } - let pathWithoutDashboardPath = path.getAsStringDangerous().split(constants_1.DASHBOARD_API)[1]; - if (pathWithoutDashboardPath.charAt(0) === "/") { - pathWithoutDashboardPath = pathWithoutDashboardPath.substring(1, pathWithoutDashboardPath.length); - } - if (pathWithoutDashboardPath.split("/")[0] === "api") { - return true; - } - return false; -} -exports.isApiPath = isApiPath; -function getApiIdIfMatched(path, method) { - if (path.getAsStringDangerous().endsWith(constants_1.VALIDATE_KEY_API) && method === "post") { - return constants_1.VALIDATE_KEY_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.SIGN_IN_API) && method === "post") { - return constants_1.SIGN_IN_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.SIGN_OUT_API) && method === "post") { - return constants_1.SIGN_OUT_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.USERS_LIST_GET_API) && method === "get") { - return constants_1.USERS_LIST_GET_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.USERS_COUNT_API) && method === "get") { - return constants_1.USERS_COUNT_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.USER_API)) { - if (method === "get" || method === "delete" || method === "put") { - return constants_1.USER_API; - } - } - if (path.getAsStringDangerous().endsWith(constants_1.USER_EMAIL_VERIFY_API)) { - if (method === "get" || method === "put") { - return constants_1.USER_EMAIL_VERIFY_API; - } - } - if (path.getAsStringDangerous().endsWith(constants_1.USER_METADATA_API)) { - if (method === "get" || method === "put") { - return constants_1.USER_METADATA_API; - } - } - if (path.getAsStringDangerous().endsWith(constants_1.USER_SESSIONS_API)) { - if (method === "get" || method === "post") { - return constants_1.USER_SESSIONS_API; - } - } - if (path.getAsStringDangerous().endsWith(constants_1.USER_PASSWORD_API) && method === "put") { - return constants_1.USER_PASSWORD_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.USER_EMAIL_VERIFY_TOKEN_API) && method === "post") { - return constants_1.USER_EMAIL_VERIFY_TOKEN_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.USER_PASSWORD_API) && method === "put") { - return constants_1.USER_PASSWORD_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.SEARCH_TAGS_API) && method === "get") { - return constants_1.SEARCH_TAGS_API; - } - if (path.getAsStringDangerous().endsWith(constants_1.DASHBOARD_ANALYTICS_API) && method === "post") { - return constants_1.DASHBOARD_ANALYTICS_API; - } - return undefined; -} -exports.getApiIdIfMatched = getApiIdIfMatched; -function sendUnauthorisedAccess(res) { - utils_1.sendNon200ResponseWithMessage(res, "Unauthorised access", 401); -} -exports.sendUnauthorisedAccess = sendUnauthorisedAccess; -function isValidRecipeId(recipeId) { - return recipeId === "emailpassword" || recipeId === "thirdparty" || recipeId === "passwordless"; -} -exports.isValidRecipeId = isValidRecipeId; -function getUserForRecipeId(userId, recipeId) { - return __awaiter(this, void 0, void 0, function* () { - let user; - let recipe; - if (recipeId === recipe_1.default.RECIPE_ID) { - try { - const userResponse = yield emailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "emailpassword"; - } - } catch (e) { - // No - op - } - if (user === undefined) { - try { - const userResponse = yield thirdpartyemailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === recipe_2.default.RECIPE_ID) { - try { - const userResponse = yield thirdparty_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdparty"; - } - } catch (e) { - // No - op - } - if (user === undefined) { - try { - const userResponse = yield thirdpartyemailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - if (user === undefined) { - try { - const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === recipe_3.default.RECIPE_ID) { - try { - const userResponse = yield passwordless_1.default.getUserById({ - userId, - }); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "passwordless"; - } - } catch (e) { - // No - op - } - if (user === undefined) { - try { - const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { firstName: "", lastName: "" }); - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } - return { - user, - recipe, - }; - }); -} -exports.getUserForRecipeId = getUserForRecipeId; -function isRecipeInitialised(recipeId) { - let isRecipeInitialised = false; - if (recipeId === "emailpassword") { - try { - recipe_1.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - if (!isRecipeInitialised) { - try { - recipe_4.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - } else if (recipeId === "passwordless") { - try { - recipe_3.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - if (!isRecipeInitialised) { - try { - recipe_5.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - } else if (recipeId === "thirdparty") { - try { - recipe_2.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - if (!isRecipeInitialised) { - try { - recipe_4.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - if (!isRecipeInitialised) { - try { - recipe_5.default.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - } - return isRecipeInitialised; -} -exports.isRecipeInitialised = isRecipeInitialised; -function validateApiKey(input) { - return __awaiter(this, void 0, void 0, function* () { - let apiKeyHeaderValue = input.req.getHeaderValue("authorization"); - // We receieve the api key as `Bearer API_KEY`, this retrieves just the key - apiKeyHeaderValue = - apiKeyHeaderValue === null || apiKeyHeaderValue === void 0 ? void 0 : apiKeyHeaderValue.split(" ")[1]; - if (apiKeyHeaderValue === undefined) { - return false; - } - return apiKeyHeaderValue === input.config.apiKey; - }); -} -exports.validateApiKey = validateApiKey; -function getApiPathWithDashboardBase(path) { - return constants_1.DASHBOARD_API + path; -} -exports.getApiPathWithDashboardBase = getApiPathWithDashboardBase; diff --git a/lib/build/recipe/emailpassword/api/emailExists.d.ts b/lib/build/recipe/emailpassword/api/emailExists.d.ts deleted file mode 100644 index 74f301a87..000000000 --- a/lib/build/recipe/emailpassword/api/emailExists.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function emailExists(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/emailpassword/api/emailExists.js b/lib/build/recipe/emailpassword/api/emailExists.js deleted file mode 100644 index 2c99dc317..000000000 --- a/lib/build/recipe/emailpassword/api/emailExists.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const utils_2 = require("../../../utils"); -function emailExists(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/47#issue-751571692 - if (apiImplementation.emailExistsGET === undefined) { - return false; - } - let email = options.req.getKeyValueFromQuery("email"); - if (email === undefined || typeof email !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the email as a GET param", - }); - } - let result = yield apiImplementation.emailExistsGET({ - email, - options, - userContext: utils_2.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = emailExists; diff --git a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.d.ts b/lib/build/recipe/emailpassword/api/generatePasswordResetToken.d.ts deleted file mode 100644 index 866cf3c64..000000000 --- a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function generatePasswordResetToken( - apiImplementation: APIInterface, - options: APIOptions -): Promise; diff --git a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.js b/lib/build/recipe/emailpassword/api/generatePasswordResetToken.js deleted file mode 100644 index 3fa0fad07..000000000 --- a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("./utils"); -const utils_3 = require("../../../utils"); -function generatePasswordResetToken(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 - if (apiImplementation.generatePasswordResetTokenPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm, - (yield options.req.getJSONBody()).formFields - ); - let result = yield apiImplementation.generatePasswordResetTokenPOST({ - formFields, - options, - userContext: utils_3.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = generatePasswordResetToken; diff --git a/lib/build/recipe/emailpassword/api/implementation.d.ts b/lib/build/recipe/emailpassword/api/implementation.d.ts deleted file mode 100644 index 402db9918..000000000 --- a/lib/build/recipe/emailpassword/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../"; -export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js deleted file mode 100644 index 805c123c0..000000000 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ /dev/null @@ -1,151 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const logger_1 = require("../../../logger"); -const session_1 = __importDefault(require("../../session")); -function getAPIImplementation() { - return { - emailExistsGET: function ({ email, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield options.recipeImplementation.getUserByEmail({ email, userContext }); - return { - status: "OK", - exists: user !== undefined, - }; - }); - }, - generatePasswordResetTokenPOST: function ({ formFields, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let email = formFields.filter((f) => f.id === "email")[0].value; - let user = yield options.recipeImplementation.getUserByEmail({ email, userContext }); - if (user === undefined) { - return { - status: "OK", - }; - } - let response = yield options.recipeImplementation.createResetPasswordToken({ - userId: user.id, - userContext, - }); - if (response.status === "UNKNOWN_USER_ID_ERROR") { - logger_1.logDebugMessage(`Password reset email not sent, unknown user id: ${user.id}`); - return { - status: "OK", - }; - } - let passwordResetLink = - options.appInfo.websiteDomain.getAsStringDangerous() + - options.appInfo.websiteBasePath.getAsStringDangerous() + - "/reset-password?token=" + - response.token + - "&rid=" + - options.recipeId; - logger_1.logDebugMessage(`Sending password reset email to ${email}`); - yield options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORD_RESET", - user, - passwordResetLink, - userContext, - }); - return { - status: "OK", - }; - }); - }, - passwordResetPOST: function ({ formFields, token, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let newPassword = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.resetPasswordUsingToken({ - token, - newPassword, - userContext, - }); - return response; - }); - }, - signInPOST: function ({ formFields, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.signIn({ email, password, userContext }); - if (response.status === "WRONG_CREDENTIALS_ERROR") { - return response; - } - let user = response.user; - let session = yield session_1.default.createNewSession( - options.req, - options.res, - user.id, - {}, - {}, - userContext - ); - return { - status: "OK", - session, - user, - }; - }); - }, - signUpPOST: function ({ formFields, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.signUp({ email, password, userContext }); - if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return response; - } - let user = response.user; - let session = yield session_1.default.createNewSession( - options.req, - options.res, - user.id, - {}, - {}, - userContext - ); - return { - status: "OK", - session, - user, - }; - }); - }, - }; -} -exports.default = getAPIImplementation; diff --git a/lib/build/recipe/emailpassword/api/passwordReset.d.ts b/lib/build/recipe/emailpassword/api/passwordReset.d.ts deleted file mode 100644 index 4b5e6641c..000000000 --- a/lib/build/recipe/emailpassword/api/passwordReset.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function passwordReset(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/emailpassword/api/passwordReset.js b/lib/build/recipe/emailpassword/api/passwordReset.js deleted file mode 100644 index dc8c29d4f..000000000 --- a/lib/build/recipe/emailpassword/api/passwordReset.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("./utils"); -const error_1 = __importDefault(require("../error")); -const utils_3 = require("../../../utils"); -function passwordReset(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 - if (apiImplementation.passwordResetPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, - (yield options.req.getJSONBody()).formFields - ); - let token = (yield options.req.getJSONBody()).token; - if (token === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the password reset token", - }); - } - if (typeof token !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "The password reset token must be a string", - }); - } - let result = yield apiImplementation.passwordResetPOST({ - formFields, - token, - options, - userContext: utils_3.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response( - options.res, - result.status === "OK" - ? { - status: "OK", - } - : result - ); - return true; - }); -} -exports.default = passwordReset; diff --git a/lib/build/recipe/emailpassword/api/signin.d.ts b/lib/build/recipe/emailpassword/api/signin.d.ts deleted file mode 100644 index 6ca49c1fc..000000000 --- a/lib/build/recipe/emailpassword/api/signin.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function signInAPI(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/emailpassword/api/signin.js b/lib/build/recipe/emailpassword/api/signin.js deleted file mode 100644 index 971c51911..000000000 --- a/lib/build/recipe/emailpassword/api/signin.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("./utils"); -const utils_3 = require("../../../utils"); -function signInAPI(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 - if (apiImplementation.signInPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.signInFeature.formFields, - (yield options.req.getJSONBody()).formFields - ); - let result = yield apiImplementation.signInPOST({ - formFields, - options, - userContext: utils_3.makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - utils_1.send200Response(options.res, { - status: "OK", - user: result.user, - }); - } else { - utils_1.send200Response(options.res, result); - } - return true; - }); -} -exports.default = signInAPI; diff --git a/lib/build/recipe/emailpassword/api/signup.d.ts b/lib/build/recipe/emailpassword/api/signup.d.ts deleted file mode 100644 index bd1fa2e88..000000000 --- a/lib/build/recipe/emailpassword/api/signup.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function signUpAPI(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/emailpassword/api/signup.js b/lib/build/recipe/emailpassword/api/signup.js deleted file mode 100644 index 986be1fe2..000000000 --- a/lib/build/recipe/emailpassword/api/signup.js +++ /dev/null @@ -1,95 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("./utils"); -const error_1 = __importDefault(require("../error")); -const utils_3 = require("../../../utils"); -function signUpAPI(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 - if (apiImplementation.signUpPOST === undefined) { - return false; - } - // step 1 - let formFields = yield utils_2.validateFormFieldsOrThrowError( - options.config.signUpFeature.formFields, - (yield options.req.getJSONBody()).formFields - ); - let result = yield apiImplementation.signUpPOST({ - formFields, - options, - userContext: utils_3.makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - utils_1.send200Response(options.res, { - status: "OK", - user: result.user, - }); - } else if (result.status === "GENERAL_ERROR") { - utils_1.send200Response(options.res, result); - } else { - throw new error_1.default({ - type: error_1.default.FIELD_ERROR, - payload: [ - { - id: "email", - error: "This email already exists. Please sign in instead.", - }, - ], - message: "Error in input formFields", - }); - } - return true; - }); -} -exports.default = signUpAPI; diff --git a/lib/build/recipe/emailpassword/api/utils.d.ts b/lib/build/recipe/emailpassword/api/utils.d.ts deleted file mode 100644 index 5579f71cc..000000000 --- a/lib/build/recipe/emailpassword/api/utils.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -import { NormalisedFormField } from "../types"; -export declare function validateFormFieldsOrThrowError( - configFormFields: NormalisedFormField[], - formFieldsRaw: any -): Promise< - { - id: string; - value: string; - }[] ->; diff --git a/lib/build/recipe/emailpassword/api/utils.js b/lib/build/recipe/emailpassword/api/utils.js deleted file mode 100644 index bbf03c787..000000000 --- a/lib/build/recipe/emailpassword/api/utils.js +++ /dev/null @@ -1,120 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateFormFieldsOrThrowError = void 0; -const error_1 = __importDefault(require("../error")); -const constants_1 = require("../constants"); -function validateFormFieldsOrThrowError(configFormFields, formFieldsRaw) { - return __awaiter(this, void 0, void 0, function* () { - // first we check syntax ---------------------------- - if (formFieldsRaw === undefined) { - throw newBadRequestError("Missing input param: formFields"); - } - if (!Array.isArray(formFieldsRaw)) { - throw newBadRequestError("formFields must be an array"); - } - let formFields = []; - for (let i = 0; i < formFieldsRaw.length; i++) { - let curr = formFieldsRaw[i]; - if (typeof curr !== "object" || curr === null) { - throw newBadRequestError("All elements of formFields must be an object"); - } - if (typeof curr.id !== "string" || curr.value === undefined) { - throw newBadRequestError("All elements of formFields must contain an 'id' and 'value' field"); - } - formFields.push(curr); - } - // we trim the email: https://github.com/supertokens/supertokens-core/issues/99 - formFields = formFields.map((field) => { - if (field.id === constants_1.FORM_FIELD_EMAIL_ID) { - return Object.assign(Object.assign({}, field), { value: field.value.trim() }); - } - return field; - }); - // then run validators through them----------------------- - yield validateFormOrThrowError(formFields, configFormFields); - return formFields; - }); -} -exports.validateFormFieldsOrThrowError = validateFormFieldsOrThrowError; -function newBadRequestError(message) { - return new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message, - }); -} -// We check that the number of fields in input and config form field is the same. -// We check that each item in the config form field is also present in the input form field -function validateFormOrThrowError(inputs, configFormFields) { - return __awaiter(this, void 0, void 0, function* () { - let validationErrors = []; - if (configFormFields.length !== inputs.length) { - throw newBadRequestError("Are you sending too many / too few formFields?"); - } - // Loop through all form fields. - for (let i = 0; i < configFormFields.length; i++) { - const field = configFormFields[i]; - // Find corresponding input value. - const input = inputs.find((i) => i.id === field.id); - // Absent or not optional empty field - if (input === undefined || (input.value === "" && !field.optional)) { - validationErrors.push({ - error: "Field is not optional", - id: field.id, - }); - } else { - // Otherwise, use validate function. - const error = yield field.validate(input.value); - // If error, add it. - if (error !== undefined) { - validationErrors.push({ - error, - id: field.id, - }); - } - } - } - if (validationErrors.length !== 0) { - throw new error_1.default({ - type: error_1.default.FIELD_ERROR, - payload: validationErrors, - message: "Error in input formFields", - }); - } - }); -} diff --git a/lib/build/recipe/emailpassword/constants.d.ts b/lib/build/recipe/emailpassword/constants.d.ts deleted file mode 100644 index 9b58b697a..000000000 --- a/lib/build/recipe/emailpassword/constants.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -export declare const FORM_FIELD_PASSWORD_ID = "password"; -export declare const FORM_FIELD_EMAIL_ID = "email"; -export declare const SIGN_UP_API = "/signup"; -export declare const SIGN_IN_API = "/signin"; -export declare const GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; -export declare const PASSWORD_RESET_API = "/user/password/reset"; -export declare const SIGNUP_EMAIL_EXISTS_API = "/signup/email/exists"; diff --git a/lib/build/recipe/emailpassword/constants.js b/lib/build/recipe/emailpassword/constants.js deleted file mode 100644 index 465baa718..000000000 --- a/lib/build/recipe/emailpassword/constants.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SIGNUP_EMAIL_EXISTS_API = exports.PASSWORD_RESET_API = exports.GENERATE_PASSWORD_RESET_TOKEN_API = exports.SIGN_IN_API = exports.SIGN_UP_API = exports.FORM_FIELD_EMAIL_ID = exports.FORM_FIELD_PASSWORD_ID = void 0; -exports.FORM_FIELD_PASSWORD_ID = "password"; -exports.FORM_FIELD_EMAIL_ID = "email"; -exports.SIGN_UP_API = "/signup"; -exports.SIGN_IN_API = "/signin"; -exports.GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; -exports.PASSWORD_RESET_API = "/user/password/reset"; -exports.SIGNUP_EMAIL_EXISTS_API = "/signup/email/exists"; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts deleted file mode 100644 index 77620015d..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -import { TypeEmailPasswordEmailDeliveryInput, User, RecipeInterface } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private recipeInterfaceImpl; - private isInServerlessEnv; - private appInfo; - private resetPasswordUsingTokenFeature; - constructor( - recipeInterfaceImpl: RecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } - ); - sendEmail: ( - input: TypeEmailPasswordEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js deleted file mode 100644 index 96f394f0d..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js +++ /dev/null @@ -1,84 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const passwordResetFunctions_1 = require("../../../passwordResetFunctions"); -class BackwardCompatibilityService { - constructor(recipeInterfaceImpl, appInfo, isInServerlessEnv, resetPasswordUsingTokenFeature) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let user = yield this.recipeInterfaceImpl.getUserById({ - userId: input.user.id, - userContext: input.userContext, - }); - if (user === undefined) { - throw Error("this should never come here"); - } - // we add this here cause the user may have overridden the sendEmail function - // to change the input email and if we don't do this, the input email - // will get reset by the getUserById call above. - user.email = input.user.email; - try { - if (!this.isInServerlessEnv) { - this.resetPasswordUsingTokenFeature - .createAndSendCustomEmail(user, input.passwordResetLink, input.userContext) - .catch((_) => {}); - } else { - // see https://github.com/supertokens/supertokens-node/pull/135 - yield this.resetPasswordUsingTokenFeature.createAndSendCustomEmail( - user, - input.passwordResetLink, - input.userContext - ); - } - } catch (_) {} - }); - this.recipeInterfaceImpl = recipeInterfaceImpl; - this.isInServerlessEnv = isInServerlessEnv; - this.appInfo = appInfo; - { - let inputCreateAndSendCustomEmail = - resetPasswordUsingTokenFeature === null || resetPasswordUsingTokenFeature === void 0 - ? void 0 - : resetPasswordUsingTokenFeature.createAndSendCustomEmail; - this.resetPasswordUsingTokenFeature = - inputCreateAndSendCustomEmail !== undefined - ? { - createAndSendCustomEmail: inputCreateAndSendCustomEmail, - } - : { - createAndSendCustomEmail: passwordResetFunctions_1.createAndSendCustomEmail(this.appInfo), - }; - } - } -} -exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/index.d.ts deleted file mode 100644 index 4de04d983..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import SMTP from "./smtp"; -export declare let SMTPService: typeof SMTP; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/index.js deleted file mode 100644 index 91700aeaf..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/index.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SMTPService = void 0; -const smtp_1 = __importDefault(require("./smtp")); -exports.SMTPService = smtp_1.default; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.d.ts deleted file mode 100644 index e17ccea3d..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { TypeEmailPasswordEmailDeliveryInput } from "../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - constructor(config: TypeInput); - sendEmail: ( - input: TypeEmailPasswordEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.js deleted file mode 100644 index e917fb0fe..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const nodemailer_1 = require("nodemailer"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const serviceImplementation_1 = require("./serviceImplementation"); -class SMTPService { - constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - yield this.serviceImpl.sendRawEmail( - Object.assign(Object.assign({}, content), { userContext: input.userContext }) - ); - }); - const transporter = nodemailer_1.createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new supertokens_js_override_1.default( - serviceImplementation_1.getServiceImplementation(transporter, config.smtpSettings.from) - ); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } -} -exports.default = SMTPService; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.d.ts deleted file mode 100644 index 34240509a..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { TypeEmailPasswordPasswordResetEmailDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/emaildelivery/services/smtp"; -export default function getPasswordResetEmailContent( - input: TypeEmailPasswordPasswordResetEmailDeliveryInput -): GetContentResult; -export declare function getPasswordResetEmailHTML(appName: string, email: string, resetLink: string): string; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.js b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.js deleted file mode 100644 index 9a6b9d98b..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.js +++ /dev/null @@ -1,931 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getPasswordResetEmailHTML = void 0; -const supertokens_1 = __importDefault(require("../../../../../supertokens")); -function getPasswordResetEmailContent(input) { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getPasswordResetEmailHTML(appName, input.user.email, input.passwordResetLink); - return { - body, - toEmail: input.user.email, - subject: "Password reset instructions", - isHtml: true, - }; -} -exports.default = getPasswordResetEmailContent; -function getPasswordResetEmailHTML(appName, email, resetLink) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
- - -
-
- -

- A password reset request for your account on - ${appName} has been received. -

- - -
-
-

- Alternatively, you can directly paste this link - in your browser
- ${resetLink} -

-
-
- - - - -
- - - - - - -
-

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} -exports.getPasswordResetEmailHTML = getPasswordResetEmailHTML; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.d.ts deleted file mode 100644 index 95fdbda32..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -import { TypeEmailPasswordEmailDeliveryInput } from "../../../../types"; -import { Transporter } from "nodemailer"; -import { ServiceInterface } from "../../../../../../ingredients/emaildelivery/services/smtp"; -export declare function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.js deleted file mode 100644 index f4ebb5266..000000000 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getServiceImplementation = void 0; -const passwordReset_1 = __importDefault(require("../passwordReset")); -function getServiceImplementation(transporter, from) { - return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (input.isHtml) { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }); - }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return passwordReset_1.default(input); - }); - }, - }; -} -exports.getServiceImplementation = getServiceImplementation; diff --git a/lib/build/recipe/emailpassword/error.d.ts b/lib/build/recipe/emailpassword/error.d.ts deleted file mode 100644 index d4dc2cf9b..000000000 --- a/lib/build/recipe/emailpassword/error.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -export default class SessionError extends STError { - static FIELD_ERROR: "FIELD_ERROR"; - constructor( - options: - | { - type: "FIELD_ERROR"; - payload: { - id: string; - error: string; - }[]; - message: string; - } - | { - type: "BAD_INPUT_ERROR"; - message: string; - } - ); -} diff --git a/lib/build/recipe/emailpassword/error.js b/lib/build/recipe/emailpassword/error.js deleted file mode 100644 index ce0b25647..000000000 --- a/lib/build/recipe/emailpassword/error.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class SessionError extends error_1.default { - constructor(options) { - super(Object.assign({}, options)); - this.fromRecipe = "emailpassword"; - } -} -exports.default = SessionError; -SessionError.FIELD_ERROR = "FIELD_ERROR"; diff --git a/lib/build/recipe/emailpassword/index.d.ts b/lib/build/recipe/emailpassword/index.d.ts deleted file mode 100644 index a23064ac6..000000000 --- a/lib/build/recipe/emailpassword/index.d.ts +++ /dev/null @@ -1,85 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { RecipeInterface, User, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static Error: typeof SuperTokensError; - static signUp( - email: string, - password: string, - userContext?: any - ): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - >; - static signIn( - email: string, - password: string, - userContext?: any - ): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - >; - static getUserById(userId: string, userContext?: any): Promise; - static getUserByEmail(email: string, userContext?: any): Promise; - static createResetPasswordToken( - userId: string, - userContext?: any - ): Promise< - | { - status: "OK"; - token: string; - } - | { - status: "UNKNOWN_USER_ID_ERROR"; - } - >; - static resetPasswordUsingToken( - token: string, - newPassword: string, - userContext?: any - ): Promise< - | { - status: "OK"; - userId?: string | undefined; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - >; - static updateEmailOrPassword(input: { - userId: string; - email?: string; - password?: string; - userContext?: any; - }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR"; - }>; - static sendEmail( - input: TypeEmailPasswordEmailDeliveryInput & { - userContext?: any; - } - ): Promise; -} -export declare let init: typeof Recipe.init; -export declare let Error: typeof SuperTokensError; -export declare let signUp: typeof Wrapper.signUp; -export declare let signIn: typeof Wrapper.signIn; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByEmail: typeof Wrapper.getUserByEmail; -export declare let createResetPasswordToken: typeof Wrapper.createResetPasswordToken; -export declare let resetPasswordUsingToken: typeof Wrapper.resetPasswordUsingToken; -export declare let updateEmailOrPassword: typeof Wrapper.updateEmailOrPassword; -export type { RecipeInterface, User, APIOptions, APIInterface }; -export declare let sendEmail: typeof Wrapper.sendEmail; diff --git a/lib/build/recipe/emailpassword/index.js b/lib/build/recipe/emailpassword/index.js deleted file mode 100644 index 83b11ce9c..000000000 --- a/lib/build/recipe/emailpassword/index.js +++ /dev/null @@ -1,122 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendEmail = exports.updateEmailOrPassword = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.getUserByEmail = exports.getUserById = exports.signIn = exports.signUp = exports.Error = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const error_1 = __importDefault(require("./error")); -class Wrapper { - static signUp(email, password, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.signUp({ - email, - password, - userContext: userContext === undefined ? {} : userContext, - }); - } - static signIn(email, password, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.signIn({ - email, - password, - userContext: userContext === undefined ? {} : userContext, - }); - } - static getUserById(userId, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - static getUserByEmail(email, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ - email, - userContext: userContext === undefined ? {} : userContext, - }); - } - static createResetPasswordToken(userId, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - static resetPasswordUsingToken(token, newPassword, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ - token, - newPassword, - userContext: userContext === undefined ? {} : userContext, - }); - } - static updateEmailOrPassword(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.updateEmailOrPassword(Object.assign({ userContext: {} }, input)); - } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - let recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return yield recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign({ userContext: {} }, input) - ); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -Wrapper.Error = error_1.default; -exports.init = Wrapper.init; -exports.Error = Wrapper.Error; -exports.signUp = Wrapper.signUp; -exports.signIn = Wrapper.signIn; -exports.getUserById = Wrapper.getUserById; -exports.getUserByEmail = Wrapper.getUserByEmail; -exports.createResetPasswordToken = Wrapper.createResetPasswordToken; -exports.resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; -exports.updateEmailOrPassword = Wrapper.updateEmailOrPassword; -exports.sendEmail = Wrapper.sendEmail; diff --git a/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts b/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts deleted file mode 100644 index 56b10c948..000000000 --- a/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { User } from "./types"; -import { NormalisedAppinfo } from "../../types"; -export declare function createAndSendCustomEmail( - appInfo: NormalisedAppinfo -): (user: User, passwordResetURLWithToken: string) => Promise; diff --git a/lib/build/recipe/emailpassword/passwordResetFunctions.js b/lib/build/recipe/emailpassword/passwordResetFunctions.js deleted file mode 100644 index 535499e39..000000000 --- a/lib/build/recipe/emailpassword/passwordResetFunctions.js +++ /dev/null @@ -1,105 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAndSendCustomEmail = void 0; -const axios_1 = __importDefault(require("axios")); -const logger_1 = require("../../logger"); -function createAndSendCustomEmail(appInfo) { - return (user, passwordResetURLWithToken) => - __awaiter(this, void 0, void 0, function* () { - // related issue: https://github.com/supertokens/supertokens-node/issues/38 - if (process.env.TEST_MODE === "testing") { - return; - } - try { - yield axios_1.default({ - method: "POST", - url: "https://api.supertokens.io/0/st/auth/password/reset", - data: { - email: user.email, - appName: appInfo.appName, - passwordResetURL: passwordResetURLWithToken, - }, - headers: { - "api-version": 0, - }, - }); - logger_1.logDebugMessage(`Password reset email sent to ${user.email}`); - } catch (error) { - logger_1.logDebugMessage("Error sending password reset email"); - if (axios_1.default.isAxiosError(error)) { - const err = error; - if (err.response) { - logger_1.logDebugMessage(`Error status: ${err.response.status}`); - logger_1.logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logger_1.logDebugMessage(`Error: ${err.message}`); - } - } else { - logger_1.logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logger_1.logDebugMessage("Logging the input below:"); - logger_1.logDebugMessage( - JSON.stringify( - { - email: user.email, - appName: appInfo.appName, - passwordResetURL: passwordResetURLWithToken, - }, - null, - 2 - ) - ); - } - }); -} -exports.createAndSendCustomEmail = createAndSendCustomEmail; diff --git a/lib/build/recipe/emailpassword/recipe.d.ts b/lib/build/recipe/emailpassword/recipe.d.ts deleted file mode 100644 index f2bd86c2c..000000000 --- a/lib/build/recipe/emailpassword/recipe.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction } from "../../types"; -import STError from "./error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { TypeEmailPasswordEmailDeliveryInput } from "./types"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - emailDelivery: EmailDeliveryIngredient; - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput | undefined, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - } - ); - static getInstanceOrThrowError(): Recipe; - static init(config?: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - _path: NormalisedURLPath, - _method: HTTPMethod - ) => Promise; - handleError: (err: STError, _request: BaseRequest, response: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; - getEmailForUserId: GetEmailForUserIdFunc; -} diff --git a/lib/build/recipe/emailpassword/recipe.js b/lib/build/recipe/emailpassword/recipe.js deleted file mode 100644 index e22893e5e..000000000 --- a/lib/build/recipe/emailpassword/recipe.js +++ /dev/null @@ -1,227 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const error_1 = __importDefault(require("./error")); -const utils_1 = require("./utils"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const constants_1 = require("./constants"); -const signup_1 = __importDefault(require("./api/signup")); -const signin_1 = __importDefault(require("./api/signin")); -const generatePasswordResetToken_1 = __importDefault(require("./api/generatePasswordResetToken")); -const passwordReset_1 = __importDefault(require("./api/passwordReset")); -const utils_2 = require("../../utils"); -const emailExists_1 = __importDefault(require("./api/emailExists")); -const recipe_1 = __importDefault(require("../emailverification/recipe")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const querier_1 = require("../../querier"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); -const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { - super(recipeId, appInfo); - // abstract instance functions below............... - this.getAPIsHandled = () => { - return [ - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_UP_API), - id: constants_1.SIGN_UP_API, - disabled: this.apiImpl.signUpPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_IN_API), - id: constants_1.SIGN_IN_API, - disabled: this.apiImpl.signInPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default( - constants_1.GENERATE_PASSWORD_RESET_TOKEN_API - ), - id: constants_1.GENERATE_PASSWORD_RESET_TOKEN_API, - disabled: this.apiImpl.generatePasswordResetTokenPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.PASSWORD_RESET_API), - id: constants_1.PASSWORD_RESET_API, - disabled: this.apiImpl.passwordResetPOST === undefined, - }, - { - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGNUP_EMAIL_EXISTS_API), - id: constants_1.SIGNUP_EMAIL_EXISTS_API, - disabled: this.apiImpl.emailExistsGET === undefined, - }, - ]; - }; - this.handleAPIRequest = (id, req, res, _path, _method) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.SIGN_UP_API) { - return yield signup_1.default(this.apiImpl, options); - } else if (id === constants_1.SIGN_IN_API) { - return yield signin_1.default(this.apiImpl, options); - } else if (id === constants_1.GENERATE_PASSWORD_RESET_TOKEN_API) { - return yield generatePasswordResetToken_1.default(this.apiImpl, options); - } else if (id === constants_1.PASSWORD_RESET_API) { - return yield passwordReset_1.default(this.apiImpl, options); - } else if (id === constants_1.SIGNUP_EMAIL_EXISTS_API) { - return yield emailExists_1.default(this.apiImpl, options); - } - return false; - }); - this.handleError = (err, _request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === Recipe.RECIPE_ID) { - if (err.type === error_1.default.FIELD_ERROR) { - return utils_2.send200Response(response, { - status: "FIELD_ERROR", - formFields: err.payload, - }); - } else { - throw err; - } - } else { - throw err; - } - }); - this.getAllCORSHeaders = () => { - return []; - }; - this.isErrorFromThisRecipe = (err) => { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - // extra instance functions below............... - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - let userInfo = yield this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }); - this.isInServerlessEnv = isInServerlessEnv; - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new emaildelivery_1.default( - this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv) - ) - : ingredients.emailDelivery; - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = recipe_1.default.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { - emailDelivery: undefined, - }); - return Recipe.instance; - } else { - throw new Error("Emailpassword recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "emailpassword"; diff --git a/lib/build/recipe/emailpassword/recipeImplementation.d.ts b/lib/build/recipe/emailpassword/recipeImplementation.d.ts deleted file mode 100644 index 86bf78a27..000000000 --- a/lib/build/recipe/emailpassword/recipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "./types"; -import { Querier } from "../../querier"; -export default function getRecipeInterface(querier: Querier): RecipeInterface; diff --git a/lib/build/recipe/emailpassword/recipeImplementation.js b/lib/build/recipe/emailpassword/recipeImplementation.js deleted file mode 100644 index 3820148ef..000000000 --- a/lib/build/recipe/emailpassword/recipeImplementation.js +++ /dev/null @@ -1,153 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier) { - return { - signUp: function ({ email, password }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/signup"), { - email, - password, - }); - if (response.status === "OK") { - return response; - } else { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - }); - }, - signIn: function ({ email, password }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/signin"), { - email, - password, - }); - if (response.status === "OK") { - return response; - } else { - return { - status: "WRONG_CREDENTIALS_ERROR", - }; - } - }); - }, - getUserById: function ({ userId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { - userId, - }); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; - } - }); - }, - getUserByEmail: function ({ email }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { - email, - }); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; - } - }); - }, - createResetPasswordToken: function ({ userId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/user/password/reset/token"), - { - userId, - } - ); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } - }); - }, - resetPasswordUsingToken: function ({ token, newPassword }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/user/password/reset"), - { - method: "token", - token, - newPassword, - } - ); - return response; - }); - }, - updateEmailOrPassword: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user"), { - userId: input.userId, - email: input.email, - password: input.password, - }); - if (response.status === "OK") { - return { - status: "OK", - }; - } else if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts deleted file mode 100644 index f997f31a7..000000000 --- a/lib/build/recipe/emailpassword/types.d.ts +++ /dev/null @@ -1,252 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; -export declare type TypeNormalisedInput = { - signUpFeature: TypeNormalisedInputSignUp; - signInFeature: TypeNormalisedInputSignIn; - getEmailDeliveryConfig: ( - recipeImpl: RecipeInterface, - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - resetPasswordUsingTokenFeature: TypeNormalisedInputResetPasswordUsingTokenFeature; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeInputFormField = { - id: string; - validate?: (value: any) => Promise; - optional?: boolean; -}; -export declare type TypeFormField = { - id: string; - value: any; -}; -export declare type TypeInputSignUp = { - formFields?: TypeInputFormField[]; -}; -export declare type NormalisedFormField = { - id: string; - validate: (value: any) => Promise; - optional: boolean; -}; -export declare type TypeNormalisedInputSignUp = { - formFields: NormalisedFormField[]; -}; -export declare type TypeNormalisedInputSignIn = { - formFields: NormalisedFormField[]; -}; -export declare type TypeInputResetPasswordUsingTokenFeature = { - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise; -}; -export declare type TypeNormalisedInputResetPasswordUsingTokenFeature = { - formFieldsForGenerateTokenForm: NormalisedFormField[]; - formFieldsForPasswordResetForm: NormalisedFormField[]; -}; -export declare type User = { - id: string; - email: string; - timeJoined: number; -}; -export declare type TypeInput = { - signUpFeature?: TypeInputSignUp; - emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type RecipeInterface = { - signUp(input: { - email: string; - password: string; - userContext: any; - }): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - >; - signIn(input: { - email: string; - password: string; - userContext: any; - }): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - >; - getUserById(input: { userId: string; userContext: any }): Promise; - getUserByEmail(input: { email: string; userContext: any }): Promise; - createResetPasswordToken(input: { - userId: string; - userContext: any; - }): Promise< - | { - status: "OK"; - token: string; - } - | { - status: "UNKNOWN_USER_ID_ERROR"; - } - >; - resetPasswordUsingToken(input: { - token: string; - newPassword: string; - userContext: any; - }): Promise< - | { - status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - >; - updateEmailOrPassword(input: { - userId: string; - email?: string; - password?: string; - userContext: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - appInfo: NormalisedAppinfo; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - emailDelivery: EmailDeliveryIngredient; -}; -export declare type APIInterface = { - emailExistsGET: - | undefined - | ((input: { - email: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); - generatePasswordResetTokenPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - >); - passwordResetPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - token: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - userId?: string; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - | GeneralErrorResponse - >); - signInPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - | GeneralErrorResponse - >); - signUpPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | GeneralErrorResponse - >); -}; -export declare type TypeEmailPasswordPasswordResetEmailDeliveryInput = { - type: "PASSWORD_RESET"; - user: { - id: string; - email: string; - }; - passwordResetLink: string; -}; -export declare type TypeEmailPasswordEmailDeliveryInput = TypeEmailPasswordPasswordResetEmailDeliveryInput; diff --git a/lib/build/recipe/emailpassword/types.js b/lib/build/recipe/emailpassword/types.js deleted file mode 100644 index a098ca1d7..000000000 --- a/lib/build/recipe/emailpassword/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/emailpassword/utils.d.ts b/lib/build/recipe/emailpassword/utils.d.ts deleted file mode 100644 index c48cdc96d..000000000 --- a/lib/build/recipe/emailpassword/utils.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput, NormalisedFormField, TypeInputFormField } from "./types"; -import { NormalisedAppinfo } from "../../types"; -export declare function validateAndNormaliseUserInput( - recipeInstance: Recipe, - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; -export declare function normaliseSignUpFormFields(formFields?: TypeInputFormField[]): NormalisedFormField[]; -export declare function defaultPasswordValidator( - value: any -): Promise< - | "Development bug: Please make sure the password field yields a string" - | "Password must contain at least 8 characters, including a number" - | "Password's length must be lesser than 100 characters" - | "Password must contain at least one alphabet" - | "Password must contain at least one number" - | undefined ->; -export declare function defaultEmailValidator( - value: any -): Promise<"Development bug: Please make sure the email field yields a string" | "Email is invalid" | undefined>; diff --git a/lib/build/recipe/emailpassword/utils.js b/lib/build/recipe/emailpassword/utils.js deleted file mode 100644 index e50c8f7bb..000000000 --- a/lib/build/recipe/emailpassword/utils.js +++ /dev/null @@ -1,258 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.defaultEmailValidator = exports.defaultPasswordValidator = exports.normaliseSignUpFormFields = exports.validateAndNormaliseUserInput = void 0; -const constants_1 = require("./constants"); -const backwardCompatibility_1 = __importDefault(require("./emaildelivery/services/backwardCompatibility")); -function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { - let signUpFeature = validateAndNormaliseSignupConfig( - recipeInstance, - appInfo, - config === undefined ? undefined : config.signUpFeature - ); - let signInFeature = validateAndNormaliseSignInConfig(recipeInstance, appInfo, signUpFeature); - let resetPasswordUsingTokenFeature = validateAndNormaliseResetPasswordUsingTokenConfig(signUpFeature); - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - function getEmailDeliveryConfig(recipeImpl, isInServerlessEnv) { - var _a; - let emailService = - (_a = config === null || config === void 0 ? void 0 : config.emailDelivery) === null || _a === void 0 - ? void 0 - : _a.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation which calls our supertokens API - */ - if (emailService === undefined) { - emailService = new backwardCompatibility_1.default( - recipeImpl, - appInfo, - isInServerlessEnv, - config === null || config === void 0 ? void 0 : config.resetPasswordUsingTokenFeature - ); - } - return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }); - } - return { - signUpFeature, - signInFeature, - resetPasswordUsingTokenFeature, - override, - getEmailDeliveryConfig, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; -function validateAndNormaliseResetPasswordUsingTokenConfig(signUpConfig) { - let formFieldsForPasswordResetForm = signUpConfig.formFields - .filter((filter) => filter.id === constants_1.FORM_FIELD_PASSWORD_ID) - .map((field) => { - return { - id: field.id, - validate: field.validate, - optional: false, - }; - }); - let formFieldsForGenerateTokenForm = signUpConfig.formFields - .filter((filter) => filter.id === constants_1.FORM_FIELD_EMAIL_ID) - .map((field) => { - return { - id: field.id, - validate: field.validate, - optional: false, - }; - }); - return { - formFieldsForPasswordResetForm, - formFieldsForGenerateTokenForm, - }; -} -function normaliseSignInFormFields(formFields) { - return formFields - .filter( - (filter) => - filter.id === constants_1.FORM_FIELD_EMAIL_ID || filter.id === constants_1.FORM_FIELD_PASSWORD_ID - ) - .map((field) => { - return { - id: field.id, - // see issue: https://github.com/supertokens/supertokens-node/issues/36 - validate: field.id === constants_1.FORM_FIELD_EMAIL_ID ? field.validate : defaultValidator, - optional: false, - }; - }); -} -function validateAndNormaliseSignInConfig(_, __, signUpConfig) { - let formFields = normaliseSignInFormFields(signUpConfig.formFields); - return { - formFields, - }; -} -function normaliseSignUpFormFields(formFields) { - let normalisedFormFields = []; - if (formFields !== undefined) { - formFields.forEach((field) => { - if (field.id === constants_1.FORM_FIELD_PASSWORD_ID) { - normalisedFormFields.push({ - id: field.id, - validate: field.validate === undefined ? defaultPasswordValidator : field.validate, - optional: false, - }); - } else if (field.id === constants_1.FORM_FIELD_EMAIL_ID) { - normalisedFormFields.push({ - id: field.id, - validate: field.validate === undefined ? defaultEmailValidator : field.validate, - optional: false, - }); - } else { - normalisedFormFields.push({ - id: field.id, - validate: field.validate === undefined ? defaultValidator : field.validate, - optional: field.optional === undefined ? false : field.optional, - }); - } - }); - } - if (normalisedFormFields.filter((field) => field.id === constants_1.FORM_FIELD_PASSWORD_ID).length === 0) { - // no password field give by user - normalisedFormFields.push({ - id: constants_1.FORM_FIELD_PASSWORD_ID, - validate: defaultPasswordValidator, - optional: false, - }); - } - if (normalisedFormFields.filter((field) => field.id === constants_1.FORM_FIELD_EMAIL_ID).length === 0) { - // no email field give by user - normalisedFormFields.push({ - id: constants_1.FORM_FIELD_EMAIL_ID, - validate: defaultEmailValidator, - optional: false, - }); - } - return normalisedFormFields; -} -exports.normaliseSignUpFormFields = normaliseSignUpFormFields; -function validateAndNormaliseSignupConfig(_, __, config) { - let formFields = normaliseSignUpFormFields(config === undefined ? undefined : config.formFields); - return { - formFields, - }; -} -function defaultValidator(_) { - return __awaiter(this, void 0, void 0, function* () { - return undefined; - }); -} -function defaultPasswordValidator(value) { - return __awaiter(this, void 0, void 0, function* () { - // length >= 8 && < 100 - // must have a number and a character - // as per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - if (typeof value !== "string") { - return "Development bug: Please make sure the password field yields a string"; - } - if (value.length < 8) { - return "Password must contain at least 8 characters, including a number"; - } - if (value.length >= 100) { - return "Password's length must be lesser than 100 characters"; - } - if (value.match(/^.*[A-Za-z]+.*$/) === null) { - return "Password must contain at least one alphabet"; - } - if (value.match(/^.*[0-9]+.*$/) === null) { - return "Password must contain at least one number"; - } - return undefined; - }); -} -exports.defaultPasswordValidator = defaultPasswordValidator; -function defaultEmailValidator(value) { - return __awaiter(this, void 0, void 0, function* () { - // We check if the email syntax is correct - // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - // Regex from https://stackoverflow.com/a/46181/3867175 - if (typeof value !== "string") { - return "Development bug: Please make sure the email field yields a string"; - } - if ( - value.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) === null - ) { - return "Email is invalid"; - } - return undefined; - }); -} -exports.defaultEmailValidator = defaultEmailValidator; diff --git a/lib/build/recipe/emailverification/api/emailVerify.d.ts b/lib/build/recipe/emailverification/api/emailVerify.d.ts deleted file mode 100644 index bd6b5b6c4..000000000 --- a/lib/build/recipe/emailverification/api/emailVerify.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function emailVerify(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/emailverification/api/emailVerify.js b/lib/build/recipe/emailverification/api/emailVerify.js deleted file mode 100644 index ef258de97..000000000 --- a/lib/build/recipe/emailverification/api/emailVerify.js +++ /dev/null @@ -1,116 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const utils_2 = require("../../../utils"); -const session_1 = __importDefault(require("../../session")); -function emailVerify(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - let result; - const userContext = utils_2.makeDefaultUserContextFromAPI(options.req); - if (utils_1.normaliseHttpMethod(options.req.getMethod()) === "post") { - // Logic according to Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 - if (apiImplementation.verifyEmailPOST === undefined) { - return false; - } - let token = (yield options.req.getJSONBody()).token; - if (token === undefined || token === null) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the email verification token", - }); - } - if (typeof token !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "The email verification token must be a string", - }); - } - const session = yield session_1.default.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [], sessionRequired: false }, - userContext - ); - let response = yield apiImplementation.verifyEmailPOST({ - token, - options, - session, - userContext, - }); - if (response.status === "OK") { - result = { status: "OK" }; - } else { - result = response; - } - } else { - if (apiImplementation.isEmailVerifiedGET === undefined) { - return false; - } - const session = yield session_1.default.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [] }, - userContext - ); - result = yield apiImplementation.isEmailVerifiedGET({ - options, - session: session, - userContext, - }); - } - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = emailVerify; diff --git a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.d.ts b/lib/build/recipe/emailverification/api/generateEmailVerifyToken.d.ts deleted file mode 100644 index 0bea1d2c2..000000000 --- a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function generateEmailVerifyToken( - apiImplementation: APIInterface, - options: APIOptions -): Promise; diff --git a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.js b/lib/build/recipe/emailverification/api/generateEmailVerifyToken.js deleted file mode 100644 index a1c6f3784..000000000 --- a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("../../../utils"); -const session_1 = __importDefault(require("../../session")); -function generateEmailVerifyToken(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 - if (apiImplementation.generateEmailVerifyTokenPOST === undefined) { - return false; - } - const userContext = utils_2.makeDefaultUserContextFromAPI(options.req); - const session = yield session_1.default.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [] }, - userContext - ); - const result = yield apiImplementation.generateEmailVerifyTokenPOST({ - options, - session: session, - userContext, - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = generateEmailVerifyToken; diff --git a/lib/build/recipe/emailverification/api/implementation.d.ts b/lib/build/recipe/emailverification/api/implementation.d.ts deleted file mode 100644 index dd40e7025..000000000 --- a/lib/build/recipe/emailverification/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../"; -export default function getAPIInterface(): APIInterface; diff --git a/lib/build/recipe/emailverification/api/implementation.js b/lib/build/recipe/emailverification/api/implementation.js deleted file mode 100644 index 0c0e84cf7..000000000 --- a/lib/build/recipe/emailverification/api/implementation.js +++ /dev/null @@ -1,166 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const logger_1 = require("../../../logger"); -const recipe_1 = __importDefault(require("../recipe")); -const emailVerificationClaim_1 = require("../emailVerificationClaim"); -const error_1 = __importDefault(require("../../session/error")); -const utils_1 = require("../utils"); -function getAPIInterface() { - return { - verifyEmailPOST: function ({ token, options, session, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const res = yield options.recipeImplementation.verifyEmailUsingToken({ token, userContext }); - if (res.status === "OK" && session !== undefined) { - try { - yield session.fetchAndSetClaim(emailVerificationClaim_1.EmailVerificationClaim, userContext); - } catch (err) { - // This should never happen, since we've just set the status above. - if (err.message === "UNKNOWN_USER_ID") { - throw new error_1.default({ - type: error_1.default.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - throw err; - } - } - return res; - }); - }, - isEmailVerifiedGET: function ({ userContext, session }) { - return __awaiter(this, void 0, void 0, function* () { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); - } - try { - yield session.fetchAndSetClaim(emailVerificationClaim_1.EmailVerificationClaim, userContext); - } catch (err) { - if (err.message === "UNKNOWN_USER_ID") { - throw new error_1.default({ - type: error_1.default.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - throw err; - } - const isVerified = yield session.getClaimValue( - emailVerificationClaim_1.EmailVerificationClaim, - userContext - ); - if (isVerified === undefined) { - throw new Error("Should never come here: EmailVerificationClaim failed to set value"); - } - return { - status: "OK", - isVerified, - }; - }); - }, - generateEmailVerifyTokenPOST: function ({ options, userContext, session }) { - return __awaiter(this, void 0, void 0, function* () { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); - } - const userId = session.getUserId(); - const emailInfo = yield recipe_1.default - .getInstanceOrThrowError() - .getEmailForUserId(userId, userContext); - if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - logger_1.logDebugMessage( - `Email verification email not sent to user ${userId} because it doesn't have an email address.` - ); - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } else if (emailInfo.status === "OK") { - let response = yield options.recipeImplementation.createEmailVerificationToken({ - userId, - email: emailInfo.email, - userContext, - }); - if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - if ((yield session.getClaimValue(emailVerificationClaim_1.EmailVerificationClaim)) !== true) { - // this can happen if the email was verified in another browser - // and this session is still outdated - and the user has not - // called the get email verification API yet. - yield session.fetchAndSetClaim( - emailVerificationClaim_1.EmailVerificationClaim, - userContext - ); - } - logger_1.logDebugMessage( - `Email verification email not sent to ${emailInfo.email} because it is already verified.` - ); - return response; - } - if ((yield session.getClaimValue(emailVerificationClaim_1.EmailVerificationClaim)) !== false) { - // this can happen if the email was unverified in another browser - // and this session is still outdated - and the user has not - // called the get email verification API yet. - yield session.fetchAndSetClaim(emailVerificationClaim_1.EmailVerificationClaim, userContext); - } - let emailVerifyLink = utils_1.getEmailVerifyLink({ - appInfo: options.appInfo, - token: response.token, - recipeId: options.recipeId, - }); - logger_1.logDebugMessage(`Sending email verification email to ${emailInfo}`); - yield options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "EMAIL_VERIFICATION", - user: { - id: userId, - email: emailInfo.email, - }, - emailVerifyLink, - userContext, - }); - return { - status: "OK", - }; - } else { - throw new error_1.default({ - type: error_1.default.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - }); - }, - }; -} -exports.default = getAPIInterface; diff --git a/lib/build/recipe/emailverification/constants.d.ts b/lib/build/recipe/emailverification/constants.d.ts deleted file mode 100644 index 7d50fa860..000000000 --- a/lib/build/recipe/emailverification/constants.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -export declare const GENERATE_EMAIL_VERIFY_TOKEN_API = "/user/email/verify/token"; -export declare const EMAIL_VERIFY_API = "/user/email/verify"; diff --git a/lib/build/recipe/emailverification/constants.js b/lib/build/recipe/emailverification/constants.js deleted file mode 100644 index b40f50dbc..000000000 --- a/lib/build/recipe/emailverification/constants.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.EMAIL_VERIFY_API = exports.GENERATE_EMAIL_VERIFY_TOKEN_API = void 0; -exports.GENERATE_EMAIL_VERIFY_TOKEN_API = "/user/email/verify/token"; -exports.EMAIL_VERIFY_API = "/user/email/verify"; diff --git a/lib/build/recipe/emailverification/emailVerificationClaim.d.ts b/lib/build/recipe/emailverification/emailVerificationClaim.d.ts deleted file mode 100644 index d29302506..000000000 --- a/lib/build/recipe/emailverification/emailVerificationClaim.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { BooleanClaim } from "../session/claims"; -import { SessionClaimValidator } from "../session"; -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -export declare class EmailVerificationClaimClass extends BooleanClaim { - constructor(); - validators: BooleanClaim["validators"] & { - isVerified: (refetchTimeOnFalseInSeconds?: number, maxAgeInSeconds?: number) => SessionClaimValidator; - }; -} -export declare const EmailVerificationClaim: EmailVerificationClaimClass; diff --git a/lib/build/recipe/emailverification/emailVerificationClaim.js b/lib/build/recipe/emailverification/emailVerificationClaim.js deleted file mode 100644 index b7375ac7a..000000000 --- a/lib/build/recipe/emailverification/emailVerificationClaim.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.EmailVerificationClaim = exports.EmailVerificationClaimClass = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const claims_1 = require("../session/claims"); -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -class EmailVerificationClaimClass extends claims_1.BooleanClaim { - constructor() { - super({ - key: "st-ev", - fetchValue(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipe = recipe_1.default.getInstanceOrThrowError(); - let emailInfo = yield recipe.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - return recipe.recipeInterfaceImpl.isEmailVerified({ - userId, - email: emailInfo.email, - userContext, - }); - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // We consider people without email addresses as validated - return true; - } else { - throw new Error("UNKNOWN_USER_ID"); - } - }); - }, - defaultMaxAgeInSeconds: 300, - }); - this.validators = Object.assign(Object.assign({}, this.validators), { - isVerified: (refetchTimeOnFalseInSeconds = 10, maxAgeInSeconds = 300) => - Object.assign(Object.assign({}, this.validators.hasValue(true, maxAgeInSeconds)), { - shouldRefetch: (payload, userContext) => { - const value = this.getValueFromPayload(payload, userContext); - return ( - value === undefined || - this.getLastRefetchTime(payload, userContext) < Date.now() - maxAgeInSeconds * 1000 || - (value === false && - this.getLastRefetchTime(payload, userContext) < - Date.now() - refetchTimeOnFalseInSeconds * 1000) - ); - }, - }), - }); - } -} -exports.EmailVerificationClaimClass = EmailVerificationClaimClass; -exports.EmailVerificationClaim = new EmailVerificationClaimClass(); diff --git a/lib/build/recipe/emailverification/emailVerificationFunctions.d.ts b/lib/build/recipe/emailverification/emailVerificationFunctions.d.ts deleted file mode 100644 index f09fbc85b..000000000 --- a/lib/build/recipe/emailverification/emailVerificationFunctions.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { User } from "./types"; -import { NormalisedAppinfo } from "../../types"; -export declare function createAndSendCustomEmail( - appInfo: NormalisedAppinfo -): (user: User, emailVerifyURLWithToken: string) => Promise; diff --git a/lib/build/recipe/emailverification/emailVerificationFunctions.js b/lib/build/recipe/emailverification/emailVerificationFunctions.js deleted file mode 100644 index 29b7b92a2..000000000 --- a/lib/build/recipe/emailverification/emailVerificationFunctions.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAndSendCustomEmail = void 0; -const axios_1 = __importDefault(require("axios")); -const logger_1 = require("../../logger"); -function createAndSendCustomEmail(appInfo) { - return (user, emailVerifyURLWithToken) => - __awaiter(this, void 0, void 0, function* () { - if (process.env.TEST_MODE === "testing") { - return; - } - try { - yield axios_1.default({ - method: "POST", - url: "https://api.supertokens.io/0/st/auth/email/verify", - data: { - email: user.email, - appName: appInfo.appName, - emailVerifyURL: emailVerifyURLWithToken, - }, - headers: { - "api-version": 0, - }, - }); - logger_1.logDebugMessage(`Email sent to ${user.email}`); - } catch (error) { - logger_1.logDebugMessage("Error sending verification email"); - if (axios_1.default.isAxiosError(error)) { - const err = error; - if (err.response) { - logger_1.logDebugMessage(`Error status: ${err.response.status}`); - logger_1.logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logger_1.logDebugMessage(`Error: ${err.message}`); - } - } else { - logger_1.logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logger_1.logDebugMessage("Logging the input below:"); - logger_1.logDebugMessage( - JSON.stringify( - { - email: user.email, - appName: appInfo.appName, - emailVerifyURL: emailVerifyURLWithToken, - }, - null, - 2 - ) - ); - } - }); -} -exports.createAndSendCustomEmail = createAndSendCustomEmail; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.d.ts deleted file mode 100644 index 6a681f11d..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -import { TypeEmailVerificationEmailDeliveryInput, User } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private appInfo; - private isInServerlessEnv; - private createAndSendCustomEmail; - constructor( - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - createAndSendCustomEmail?: ( - user: User, - emailVerificationURLWithToken: string, - userContext: any - ) => Promise - ); - sendEmail: ( - input: TypeEmailVerificationEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.js deleted file mode 100644 index 67e5311b1..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.js +++ /dev/null @@ -1,60 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const emailVerificationFunctions_1 = require("../../../emailVerificationFunctions"); -class BackwardCompatibilityService { - constructor(appInfo, isInServerlessEnv, createAndSendCustomEmail) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - try { - if (!this.isInServerlessEnv) { - this.createAndSendCustomEmail( - input.user, - input.emailVerifyLink, - input.userContext - ).catch((_) => {}); - } else { - // see https://github.com/supertokens/supertokens-node/pull/135 - yield this.createAndSendCustomEmail(input.user, input.emailVerifyLink, input.userContext); - } - } catch (_) {} - }); - this.appInfo = appInfo; - this.isInServerlessEnv = isInServerlessEnv; - this.createAndSendCustomEmail = - createAndSendCustomEmail === undefined - ? emailVerificationFunctions_1.createAndSendCustomEmail(this.appInfo) - : createAndSendCustomEmail; - } -} -exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/index.d.ts b/lib/build/recipe/emailverification/emaildelivery/services/index.d.ts deleted file mode 100644 index 4de04d983..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import SMTP from "./smtp"; -export declare let SMTPService: typeof SMTP; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/index.js b/lib/build/recipe/emailverification/emaildelivery/services/index.js deleted file mode 100644 index 91700aeaf..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/index.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SMTPService = void 0; -const smtp_1 = __importDefault(require("./smtp")); -exports.SMTPService = smtp_1.default; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/emailVerify.d.ts b/lib/build/recipe/emailverification/emaildelivery/services/smtp/emailVerify.d.ts deleted file mode 100644 index c28581ed2..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/emailVerify.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/emaildelivery/services/smtp"; -export default function getEmailVerifyEmailContent(input: TypeEmailVerificationEmailDeliveryInput): GetContentResult; -export declare function getEmailVerifyEmailHTML(appName: string, email: string, verificationLink: string): string; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/emailVerify.js b/lib/build/recipe/emailverification/emaildelivery/services/smtp/emailVerify.js deleted file mode 100644 index 521c0735f..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/emailVerify.js +++ /dev/null @@ -1,933 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getEmailVerifyEmailHTML = void 0; -const supertokens_1 = __importDefault(require("../../../../../supertokens")); -function getEmailVerifyEmailContent(input) { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getEmailVerifyEmailHTML(appName, input.user.email, input.emailVerifyLink); - return { - body, - toEmail: input.user.email, - subject: "Email verification instructions", - isHtml: true, - }; -} -exports.default = getEmailVerifyEmailContent; -function getEmailVerifyEmailHTML(appName, email, verificationLink) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
- - -
-
- -

- Please verify your email address for ${appName} - by clicking the button below.

- - -
-
-

- Alternatively, you can directly paste this link - in your browser
- ${verificationLink} -

-
-
- - - - -
-
- -
- - - - - -
- - - - - - -
- - -

- This email is meant for ${email} -

-
-
- -
- -
-
- - - - `; -} -exports.getEmailVerifyEmailHTML = getEmailVerifyEmailHTML; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.d.ts deleted file mode 100644 index c3030da4e..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - constructor(config: TypeInput); - sendEmail: ( - input: TypeEmailVerificationEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.js b/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.js deleted file mode 100644 index e917fb0fe..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const nodemailer_1 = require("nodemailer"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const serviceImplementation_1 = require("./serviceImplementation"); -class SMTPService { - constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - yield this.serviceImpl.sendRawEmail( - Object.assign(Object.assign({}, content), { userContext: input.userContext }) - ); - }); - const transporter = nodemailer_1.createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new supertokens_js_override_1.default( - serviceImplementation_1.getServiceImplementation(transporter, config.smtpSettings.from) - ); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } -} -exports.default = SMTPService; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.d.ts b/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.d.ts deleted file mode 100644 index 3f668725f..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; -import { Transporter } from "nodemailer"; -import { ServiceInterface } from "../../../../../ingredients/emaildelivery/services/smtp"; -export declare function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface; diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.js b/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.js deleted file mode 100644 index 8efb6167a..000000000 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getServiceImplementation = void 0; -const emailVerify_1 = __importDefault(require("./emailVerify")); -function getServiceImplementation(transporter, from) { - return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (input.isHtml) { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }); - }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return emailVerify_1.default(input); - }); - }, - }; -} -exports.getServiceImplementation = getServiceImplementation; diff --git a/lib/build/recipe/emailverification/error.d.ts b/lib/build/recipe/emailverification/error.d.ts deleted file mode 100644 index 486758b61..000000000 --- a/lib/build/recipe/emailverification/error.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -export default class SessionError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }); -} diff --git a/lib/build/recipe/emailverification/error.js b/lib/build/recipe/emailverification/error.js deleted file mode 100644 index b0baf2c94..000000000 --- a/lib/build/recipe/emailverification/error.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class SessionError extends error_1.default { - constructor(options) { - super(Object.assign({}, options)); - this.fromRecipe = "emailverification"; - } -} -exports.default = SessionError; diff --git a/lib/build/recipe/emailverification/index.d.ts b/lib/build/recipe/emailverification/index.d.ts deleted file mode 100644 index 9262ec6ec..000000000 --- a/lib/build/recipe/emailverification/index.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { RecipeInterface, APIOptions, APIInterface, User, TypeEmailVerificationEmailDeliveryInput } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static Error: typeof SuperTokensError; - static EmailVerificationClaim: import("./emailVerificationClaim").EmailVerificationClaimClass; - static createEmailVerificationToken( - userId: string, - email?: string, - userContext?: any - ): Promise< - | { - status: "OK"; - token: string; - } - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; - } - >; - static verifyEmailUsingToken( - token: string, - userContext?: any - ): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; - } - >; - static isEmailVerified(userId: string, email?: string, userContext?: any): Promise; - static revokeEmailVerificationTokens( - userId: string, - email?: string, - userContext?: any - ): Promise<{ - status: string; - }>; - static unverifyEmail( - userId: string, - email?: string, - userContext?: any - ): Promise<{ - status: string; - }>; - static sendEmail( - input: TypeEmailVerificationEmailDeliveryInput & { - userContext?: any; - } - ): Promise; -} -export declare let init: typeof Recipe.init; -export declare let Error: typeof SuperTokensError; -export declare let createEmailVerificationToken: typeof Wrapper.createEmailVerificationToken; -export declare let verifyEmailUsingToken: typeof Wrapper.verifyEmailUsingToken; -export declare let isEmailVerified: typeof Wrapper.isEmailVerified; -export declare let revokeEmailVerificationTokens: typeof Wrapper.revokeEmailVerificationTokens; -export declare let unverifyEmail: typeof Wrapper.unverifyEmail; -export type { RecipeInterface, APIOptions, APIInterface, User }; -export declare let sendEmail: typeof Wrapper.sendEmail; -export { EmailVerificationClaim } from "./emailVerificationClaim"; diff --git a/lib/build/recipe/emailverification/index.js b/lib/build/recipe/emailverification/index.js deleted file mode 100644 index 162e73e33..000000000 --- a/lib/build/recipe/emailverification/index.js +++ /dev/null @@ -1,186 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.EmailVerificationClaim = exports.sendEmail = exports.unverifyEmail = exports.revokeEmailVerificationTokens = exports.isEmailVerified = exports.verifyEmailUsingToken = exports.createEmailVerificationToken = exports.Error = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const error_1 = __importDefault(require("./error")); -const emailVerificationClaim_1 = require("./emailVerificationClaim"); -class Wrapper { - static createEmailVerificationToken(userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - return yield recipeInstance.recipeInterfaceImpl.createEmailVerificationToken({ - userId, - email: email, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static verifyEmailUsingToken(token, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ - token, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static isEmailVerified(userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - return true; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - return yield recipeInstance.recipeInterfaceImpl.isEmailVerified({ - userId, - email: email, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static revokeEmailVerificationTokens(userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - // If the dev wants to delete the tokens for an old email address of the user they can pass the address - // but redeeming those tokens would have no effect on isEmailVerified called without the old address - // so in general that is not necessary either. - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // This only happens for phone based passwordless users (or if the user added a custom getEmailForUserId) - // We can return OK here, since there is no way to create an email verification token - // if getEmailForUserId returns EMAIL_DOES_NOT_EXIST_ERROR. - return { - status: "OK", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - return yield recipeInstance.recipeInterfaceImpl.revokeEmailVerificationTokens({ - userId, - email: email, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static unverifyEmail(userId, email, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = yield recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // Here we are returning OK since that's how it used to work, but a later call to isVerified will still return true - return { - status: "OK", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - return yield recipeInstance.recipeInterfaceImpl.unverifyEmail({ - userId, - email: email, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - let recipeInstance = recipe_1.default.getInstanceOrThrowError(); - return yield recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign({ userContext: {} }, input) - ); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -Wrapper.Error = error_1.default; -Wrapper.EmailVerificationClaim = emailVerificationClaim_1.EmailVerificationClaim; -exports.init = Wrapper.init; -exports.Error = Wrapper.Error; -exports.createEmailVerificationToken = Wrapper.createEmailVerificationToken; -exports.verifyEmailUsingToken = Wrapper.verifyEmailUsingToken; -exports.isEmailVerified = Wrapper.isEmailVerified; -exports.revokeEmailVerificationTokens = Wrapper.revokeEmailVerificationTokens; -exports.unverifyEmail = Wrapper.unverifyEmail; -exports.sendEmail = Wrapper.sendEmail; -var emailVerificationClaim_2 = require("./emailVerificationClaim"); -Object.defineProperty(exports, "EmailVerificationClaim", { - enumerable: true, - get: function () { - return emailVerificationClaim_2.EmailVerificationClaim; - }, -}); diff --git a/lib/build/recipe/emailverification/recipe.d.ts b/lib/build/recipe/emailverification/recipe.d.ts deleted file mode 100644 index 6cb431f31..000000000 --- a/lib/build/recipe/emailverification/recipe.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForUserIdFunc } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import STError from "./error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { TypeEmailVerificationEmailDeliveryInput } from "./types"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - emailDelivery: EmailDeliveryIngredient; - getEmailForUserIdFuncsFromOtherRecipes: GetEmailForUserIdFunc[]; - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - } - ); - static getInstanceOrThrowError(): Recipe; - static getInstance(): Recipe | undefined; - static init(config: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - _: NormalisedURLPath, - __: HTTPMethod - ) => Promise; - handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; - getEmailForUserId: GetEmailForUserIdFunc; - addGetEmailForUserIdFunc: (func: GetEmailForUserIdFunc) => void; -} diff --git a/lib/build/recipe/emailverification/recipe.js b/lib/build/recipe/emailverification/recipe.js deleted file mode 100644 index 398fef214..000000000 --- a/lib/build/recipe/emailverification/recipe.js +++ /dev/null @@ -1,211 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const error_1 = __importDefault(require("./error")); -const utils_1 = require("./utils"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const constants_1 = require("./constants"); -const generateEmailVerifyToken_1 = __importDefault(require("./api/generateEmailVerifyToken")); -const emailVerify_1 = __importDefault(require("./api/emailVerify")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const querier_1 = require("../../querier"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); -const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); -const recipe_1 = __importDefault(require("../session/recipe")); -const emailVerificationClaim_1 = require("./emailVerificationClaim"); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { - super(recipeId, appInfo); - this.getEmailForUserIdFuncsFromOtherRecipes = []; - // abstract instance functions below............... - this.getAPIsHandled = () => { - return [ - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default( - constants_1.GENERATE_EMAIL_VERIFY_TOKEN_API - ), - id: constants_1.GENERATE_EMAIL_VERIFY_TOKEN_API, - disabled: this.apiImpl.generateEmailVerifyTokenPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.EMAIL_VERIFY_API), - id: constants_1.EMAIL_VERIFY_API, - disabled: this.apiImpl.verifyEmailPOST === undefined, - }, - { - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.EMAIL_VERIFY_API), - id: constants_1.EMAIL_VERIFY_API, - disabled: this.apiImpl.isEmailVerifiedGET === undefined, - }, - ]; - }; - this.handleAPIRequest = (id, req, res, _, __) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.GENERATE_EMAIL_VERIFY_TOKEN_API) { - return yield generateEmailVerifyToken_1.default(this.apiImpl, options); - } else { - return yield emailVerify_1.default(this.apiImpl, options); - } - }); - this.handleError = (err, _, __) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); - this.getAllCORSHeaders = () => { - return []; - }; - this.isErrorFromThisRecipe = (err) => { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - if (this.config.getEmailForUserId !== undefined) { - const userRes = yield this.config.getEmailForUserId(userId, userContext); - if (userRes.status !== "UNKNOWN_USER_ID_ERROR") { - return userRes; - } - } - for (const getEmailForUserId of this.getEmailForUserIdFuncsFromOtherRecipes) { - const res = yield getEmailForUserId(userId, userContext); - if (res.status !== "UNKNOWN_USER_ID_ERROR") { - return res; - } - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }); - this.addGetEmailForUserIdFunc = (func) => { - this.getEmailForUserIdFuncsFromOtherRecipes.push(func); - }; - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new emaildelivery_1.default(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) - : ingredients.emailDelivery; - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static getInstance() { - return Recipe.instance; - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { - emailDelivery: undefined, - }); - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - recipe_1.default - .getInstanceOrThrowError() - .addClaimFromOtherRecipe(emailVerificationClaim_1.EmailVerificationClaim); - if (config.mode === "REQUIRED") { - recipe_1.default - .getInstanceOrThrowError() - .addClaimValidatorFromOtherRecipe( - emailVerificationClaim_1.EmailVerificationClaim.validators.isVerified() - ); - } - }); - return Recipe.instance; - } else { - throw new Error( - "Emailverification recipe has already been initialised. Please check your code for bugs." - ); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "emailverification"; diff --git a/lib/build/recipe/emailverification/recipeImplementation.d.ts b/lib/build/recipe/emailverification/recipeImplementation.d.ts deleted file mode 100644 index 6a2182ed3..000000000 --- a/lib/build/recipe/emailverification/recipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "./"; -import { Querier } from "../../querier"; -export default function getRecipeInterface(querier: Querier): RecipeInterface; diff --git a/lib/build/recipe/emailverification/recipeImplementation.js b/lib/build/recipe/emailverification/recipeImplementation.js deleted file mode 100644 index 99489ca51..000000000 --- a/lib/build/recipe/emailverification/recipeImplementation.js +++ /dev/null @@ -1,122 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier) { - return { - createEmailVerificationToken: function ({ userId, email }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/user/email/verify/token"), - { - userId, - email, - } - ); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } - }); - }, - verifyEmailUsingToken: function ({ token }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/user/email/verify"), - { - method: "token", - token, - } - ); - if (response.status === "OK") { - return { - status: "OK", - user: { - id: response.userId, - email: response.email, - }, - }; - } else { - return { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR", - }; - } - }); - }, - isEmailVerified: function ({ userId, email }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/user/email/verify"), - { - userId, - email, - } - ); - return response.isVerified; - }); - }, - revokeEmailVerificationTokens: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/user/email/verify/token/remove"), - { - userId: input.userId, - email: input.email, - } - ); - return { status: "OK" }; - }); - }, - unverifyEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/user/email/verify/remove"), { - userId: input.userId, - email: input.email, - }); - return { status: "OK" }; - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/emailverification/types.d.ts b/lib/build/recipe/emailverification/types.d.ts deleted file mode 100644 index 27bb17bd3..000000000 --- a/lib/build/recipe/emailverification/types.d.ts +++ /dev/null @@ -1,182 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; -import { SessionContainerInterface } from "../session/types"; -export declare type TypeInput = { - mode: "REQUIRED" | "OPTIONAL"; - emailDelivery?: EmailDeliveryTypeInput; - getEmailForUserId?: ( - userId: string, - userContext: any - ) => Promise< - | { - status: "OK"; - email: string; - } - | { - status: "EMAIL_DOES_NOT_EXIST_ERROR" | "UNKNOWN_USER_ID_ERROR"; - } - >; - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: (user: User, emailVerificationURLWithToken: string, userContext: any) => Promise; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - mode: "REQUIRED" | "OPTIONAL"; - getEmailDeliveryConfig: ( - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - getEmailForUserId?: ( - userId: string, - userContext: any - ) => Promise< - | { - status: "OK"; - email: string; - } - | { - status: "EMAIL_DOES_NOT_EXIST_ERROR" | "UNKNOWN_USER_ID_ERROR"; - } - >; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type User = { - id: string; - email: string; -}; -export declare type RecipeInterface = { - createEmailVerificationToken(input: { - userId: string; - email: string; - userContext: any; - }): Promise< - | { - status: "OK"; - token: string; - } - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; - } - >; - verifyEmailUsingToken(input: { - token: string; - userContext: any; - }): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; - } - >; - isEmailVerified(input: { userId: string; email: string; userContext: any }): Promise; - revokeEmailVerificationTokens(input: { - userId: string; - email: string; - userContext: any; - }): Promise<{ - status: "OK"; - }>; - unverifyEmail(input: { - userId: string; - email: string; - userContext: any; - }): Promise<{ - status: "OK"; - }>; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - appInfo: NormalisedAppinfo; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - emailDelivery: EmailDeliveryIngredient; -}; -export declare type APIInterface = { - verifyEmailPOST: - | undefined - | ((input: { - token: string; - options: APIOptions; - userContext: any; - session?: SessionContainerInterface; - }) => Promise< - | { - status: "OK"; - user: User; - } - | { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; - } - | GeneralErrorResponse - >); - isEmailVerifiedGET: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - session: SessionContainerInterface; - }) => Promise< - | { - status: "OK"; - isVerified: boolean; - } - | GeneralErrorResponse - >); - generateEmailVerifyTokenPOST: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - session: SessionContainerInterface; - }) => Promise< - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR" | "OK"; - } - | GeneralErrorResponse - >); -}; -export declare type TypeEmailVerificationEmailDeliveryInput = { - type: "EMAIL_VERIFICATION"; - user: { - id: string; - email: string; - }; - emailVerifyLink: string; -}; -export declare type GetEmailForUserIdFunc = ( - userId: string, - userContext: any -) => Promise< - | { - status: "OK"; - email: string; - } - | { - status: "EMAIL_DOES_NOT_EXIST_ERROR" | "UNKNOWN_USER_ID_ERROR"; - } ->; diff --git a/lib/build/recipe/emailverification/types.js b/lib/build/recipe/emailverification/types.js deleted file mode 100644 index a098ca1d7..000000000 --- a/lib/build/recipe/emailverification/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/emailverification/utils.d.ts b/lib/build/recipe/emailverification/utils.d.ts deleted file mode 100644 index 627176f4a..000000000 --- a/lib/build/recipe/emailverification/utils.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput } from "./types"; -import { NormalisedAppinfo } from "../../types"; -export declare function validateAndNormaliseUserInput( - _: Recipe, - appInfo: NormalisedAppinfo, - config: TypeInput -): TypeNormalisedInput; -export declare function getEmailVerifyLink(input: { - appInfo: NormalisedAppinfo; - token: string; - recipeId: string; -}): string; diff --git a/lib/build/recipe/emailverification/utils.js b/lib/build/recipe/emailverification/utils.js deleted file mode 100644 index 26dc9f073..000000000 --- a/lib/build/recipe/emailverification/utils.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getEmailVerifyLink = exports.validateAndNormaliseUserInput = void 0; -const backwardCompatibility_1 = __importDefault(require("./emaildelivery/services/backwardCompatibility")); -function validateAndNormaliseUserInput(_, appInfo, config) { - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config.override - ); - function getEmailDeliveryConfig(isInServerlessEnv) { - var _a; - let emailService = (_a = config.emailDelivery) === null || _a === void 0 ? void 0 : _a.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation which calls our supertokens API - */ - if (emailService === undefined) { - emailService = new backwardCompatibility_1.default( - appInfo, - isInServerlessEnv, - config.createAndSendCustomEmail - ); - } - return Object.assign(Object.assign({}, config.emailDelivery), { - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }); - } - return { - mode: config.mode, - getEmailForUserId: config.getEmailForUserId, - override, - getEmailDeliveryConfig, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; -function getEmailVerifyLink(input) { - return ( - input.appInfo.websiteDomain.getAsStringDangerous() + - input.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify-email" + - "?token=" + - input.token + - "&rid=" + - input.recipeId - ); -} -exports.getEmailVerifyLink = getEmailVerifyLink; diff --git a/lib/build/recipe/jwt/api/getJWKS.d.ts b/lib/build/recipe/jwt/api/getJWKS.d.ts deleted file mode 100644 index 7b983911b..000000000 --- a/lib/build/recipe/jwt/api/getJWKS.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export default function getJWKS(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/jwt/api/getJWKS.js b/lib/build/recipe/jwt/api/getJWKS.js deleted file mode 100644 index b2e33b23b..000000000 --- a/lib/build/recipe/jwt/api/getJWKS.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("../../../utils"); -function getJWKS(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.getJWKSGET === undefined) { - return false; - } - let result = yield apiImplementation.getJWKSGET({ - options, - userContext: utils_2.makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - options.res.setHeader("Access-Control-Allow-Origin", "*", false); - utils_1.send200Response(options.res, { keys: result.keys }); - } else { - utils_1.send200Response(options.res, result); - } - return true; - }); -} -exports.default = getJWKS; diff --git a/lib/build/recipe/jwt/api/implementation.d.ts b/lib/build/recipe/jwt/api/implementation.d.ts deleted file mode 100644 index 0218549fa..000000000 --- a/lib/build/recipe/jwt/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../types"; -export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/jwt/api/implementation.js b/lib/build/recipe/jwt/api/implementation.js deleted file mode 100644 index 88600ac6e..000000000 --- a/lib/build/recipe/jwt/api/implementation.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getAPIImplementation() { - return { - getJWKSGET: function ({ options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - return yield options.recipeImplementation.getJWKS({ userContext }); - }); - }, - }; -} -exports.default = getAPIImplementation; diff --git a/lib/build/recipe/jwt/constants.d.ts b/lib/build/recipe/jwt/constants.d.ts deleted file mode 100644 index 719f84e81..000000000 --- a/lib/build/recipe/jwt/constants.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-nocheck -export declare const GET_JWKS_API = "/jwt/jwks.json"; diff --git a/lib/build/recipe/jwt/constants.js b/lib/build/recipe/jwt/constants.js deleted file mode 100644 index b21a20fa1..000000000 --- a/lib/build/recipe/jwt/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.GET_JWKS_API = void 0; -exports.GET_JWKS_API = "/jwt/jwks.json"; diff --git a/lib/build/recipe/jwt/index.d.ts b/lib/build/recipe/jwt/index.d.ts deleted file mode 100644 index 274bd280b..000000000 --- a/lib/build/recipe/jwt/index.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import { APIInterface, RecipeInterface, APIOptions, JsonWebKey } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static createJWT( - payload: any, - validitySeconds?: number, - userContext?: any - ): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - static getJWKS( - userContext?: any - ): Promise<{ - status: "OK"; - keys: JsonWebKey[]; - }>; -} -export declare let init: typeof Recipe.init; -export declare let createJWT: typeof Wrapper.createJWT; -export declare let getJWKS: typeof Wrapper.getJWKS; -export type { APIInterface, APIOptions, RecipeInterface, JsonWebKey }; diff --git a/lib/build/recipe/jwt/index.js b/lib/build/recipe/jwt/index.js deleted file mode 100644 index 7b1184695..000000000 --- a/lib/build/recipe/jwt/index.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getJWKS = exports.createJWT = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -class Wrapper { - static createJWT(payload, validitySeconds, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ - payload, - validitySeconds, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static getJWKS(userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, - }); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -exports.init = Wrapper.init; -exports.createJWT = Wrapper.createJWT; -exports.getJWKS = Wrapper.getJWKS; diff --git a/lib/build/recipe/jwt/recipe.d.ts b/lib/build/recipe/jwt/recipe.d.ts deleted file mode 100644 index 59cd100e9..000000000 --- a/lib/build/recipe/jwt/recipe.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import normalisedURLPath from "../../normalisedURLPath"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -export default class Recipe extends RecipeModule { - static RECIPE_ID: string; - private static instance; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); - static getInstanceOrThrowError(): Recipe; - static init(config?: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled(): APIHandled[]; - handleAPIRequest: ( - _: string, - req: BaseRequest, - res: BaseResponse, - __: normalisedURLPath, - ___: HTTPMethod - ) => Promise; - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise; - getAllCORSHeaders(): string[]; - isErrorFromThisRecipe(err: any): err is error; -} diff --git a/lib/build/recipe/jwt/recipe.js b/lib/build/recipe/jwt/recipe.js deleted file mode 100644 index acf557c24..000000000 --- a/lib/build/recipe/jwt/recipe.js +++ /dev/null @@ -1,141 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const querier_1 = require("../../querier"); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const getJWKS_1 = __importDefault(require("./api/getJWKS")); -const implementation_1 = __importDefault(require("./api/implementation")); -const constants_1 = require("./constants"); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const utils_1 = require("./utils"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config) { - super(recipeId, appInfo); - this.handleAPIRequest = (_, req, res, __, ___) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - }; - return yield getJWKS_1.default(this.apiImpl, options); - }); - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default( - querier_1.Querier.getNewInstanceOrThrowError(recipeId), - this.config, - appInfo - ) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - /* Init functions */ - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("JWT recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - /* RecipeModule functions */ - getAPIsHandled() { - return [ - { - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.GET_JWKS_API), - id: constants_1.GET_JWKS_API, - disabled: this.apiImpl.getJWKSGET === undefined, - }, - ]; - } - handleError(error, _, __) { - throw error; - } - getAllCORSHeaders() { - return []; - } - isErrorFromThisRecipe(err) { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - } -} -exports.default = Recipe; -Recipe.RECIPE_ID = "jwt"; -Recipe.instance = undefined; diff --git a/lib/build/recipe/jwt/recipeImplementation.d.ts b/lib/build/recipe/jwt/recipeImplementation.d.ts deleted file mode 100644 index 5109fbcb1..000000000 --- a/lib/build/recipe/jwt/recipeImplementation.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { Querier } from "../../querier"; -import { NormalisedAppinfo } from "../../types"; -import { RecipeInterface, TypeNormalisedInput } from "./types"; -export default function getRecipeInterface( - querier: Querier, - config: TypeNormalisedInput, - appInfo: NormalisedAppinfo -): RecipeInterface; diff --git a/lib/build/recipe/jwt/recipeImplementation.js b/lib/build/recipe/jwt/recipeImplementation.js deleted file mode 100644 index 3b4f0aff8..000000000 --- a/lib/build/recipe/jwt/recipeImplementation.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier, config, appInfo) { - return { - createJWT: function ({ payload, validitySeconds }) { - return __awaiter(this, void 0, void 0, function* () { - if (validitySeconds === undefined) { - // If the user does not provide a validity to this function and the config validity is also undefined, use 100 years (in seconds) - validitySeconds = config.jwtValiditySeconds; - } - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/jwt"), { - payload: payload !== null && payload !== void 0 ? payload : {}, - validity: validitySeconds, - algorithm: "RS256", - jwksDomain: appInfo.apiDomain.getAsStringDangerous(), - }); - if (response.status === "OK") { - return { - status: "OK", - jwt: response.jwt, - }; - } else { - return { - status: "UNSUPPORTED_ALGORITHM_ERROR", - }; - } - }); - }, - getJWKS: function () { - return __awaiter(this, void 0, void 0, function* () { - return yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/jwt/jwks"), {}); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/jwt/types.d.ts b/lib/build/recipe/jwt/types.d.ts deleted file mode 100644 index fc400eff6..000000000 --- a/lib/build/recipe/jwt/types.d.ts +++ /dev/null @@ -1,75 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse } from "../../types"; -export declare type JsonWebKey = { - kty: string; - kid: string; - n: string; - e: string; - alg: string; - use: string; -}; -export declare type TypeInput = { - jwtValiditySeconds?: number; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - jwtValiditySeconds: number; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; -}; -export declare type RecipeInterface = { - createJWT(input: { - payload?: any; - validitySeconds?: number; - userContext: any; - }): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - getJWKS(input: { - userContext: any; - }): Promise<{ - status: "OK"; - keys: JsonWebKey[]; - }>; -}; -export declare type APIInterface = { - getJWKSGET: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - keys: JsonWebKey[]; - } - | GeneralErrorResponse - >); -}; diff --git a/lib/build/recipe/jwt/types.js b/lib/build/recipe/jwt/types.js deleted file mode 100644 index a098ca1d7..000000000 --- a/lib/build/recipe/jwt/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/jwt/utils.d.ts b/lib/build/recipe/jwt/utils.d.ts deleted file mode 100644 index 4025b1b44..000000000 --- a/lib/build/recipe/jwt/utils.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; diff --git a/lib/build/recipe/jwt/utils.js b/lib/build/recipe/jwt/utils.js deleted file mode 100644 index 9c38d23ce..000000000 --- a/lib/build/recipe/jwt/utils.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; -function validateAndNormaliseUserInput(_, __, config) { - var _a; - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - return { - jwtValiditySeconds: - (_a = config === null || config === void 0 ? void 0 : config.jwtValiditySeconds) !== null && _a !== void 0 - ? _a - : 3153600000, - override, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.d.ts b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.d.ts deleted file mode 100644 index e1cd1cd3b..000000000 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../types"; -export default function getOpenIdDiscoveryConfiguration( - apiImplementation: APIInterface, - options: APIOptions -): Promise; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js deleted file mode 100644 index 9fd8c6e31..000000000 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js +++ /dev/null @@ -1,71 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const utils_1 = require("../../../utils"); -const utils_2 = require("../../../utils"); -function getOpenIdDiscoveryConfiguration(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.getOpenIdDiscoveryConfigurationGET === undefined) { - return false; - } - let result = yield apiImplementation.getOpenIdDiscoveryConfigurationGET({ - options, - userContext: utils_2.makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - options.res.setHeader("Access-Control-Allow-Origin", "*", false); - utils_1.send200Response(options.res, { - issuer: result.issuer, - jwks_uri: result.jwks_uri, - }); - } else { - utils_1.send200Response(options.res, result); - } - return true; - }); -} -exports.default = getOpenIdDiscoveryConfiguration; diff --git a/lib/build/recipe/openid/api/implementation.d.ts b/lib/build/recipe/openid/api/implementation.d.ts deleted file mode 100644 index 0218549fa..000000000 --- a/lib/build/recipe/openid/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../types"; -export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/openid/api/implementation.js b/lib/build/recipe/openid/api/implementation.js deleted file mode 100644 index 81609c3b2..000000000 --- a/lib/build/recipe/openid/api/implementation.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getAPIImplementation() { - return { - getOpenIdDiscoveryConfigurationGET: function ({ options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - return yield options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); - }); - }, - }; -} -exports.default = getAPIImplementation; diff --git a/lib/build/recipe/openid/constants.d.ts b/lib/build/recipe/openid/constants.d.ts deleted file mode 100644 index 241a857f7..000000000 --- a/lib/build/recipe/openid/constants.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @ts-nocheck -export declare const GET_DISCOVERY_CONFIG_URL = "/.well-known/openid-configuration"; diff --git a/lib/build/recipe/openid/constants.js b/lib/build/recipe/openid/constants.js deleted file mode 100644 index 067eee64a..000000000 --- a/lib/build/recipe/openid/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.GET_DISCOVERY_CONFIG_URL = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -exports.GET_DISCOVERY_CONFIG_URL = "/.well-known/openid-configuration"; diff --git a/lib/build/recipe/openid/index.d.ts b/lib/build/recipe/openid/index.d.ts deleted file mode 100644 index c84226a59..000000000 --- a/lib/build/recipe/openid/index.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck -import OpenIdRecipe from "./recipe"; -export default class OpenIdRecipeWrapper { - static init: typeof OpenIdRecipe.init; - static getOpenIdDiscoveryConfiguration( - userContext?: any - ): Promise<{ - status: "OK"; - issuer: string; - jwks_uri: string; - }>; - static createJWT( - payload?: any, - validitySeconds?: number, - userContext?: any - ): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - static getJWKS( - userContext?: any - ): Promise<{ - status: "OK"; - keys: import("../jwt").JsonWebKey[]; - }>; -} -export declare let init: typeof OpenIdRecipe.init; -export declare let getOpenIdDiscoveryConfiguration: typeof OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration; -export declare let createJWT: typeof OpenIdRecipeWrapper.createJWT; -export declare let getJWKS: typeof OpenIdRecipeWrapper.getJWKS; diff --git a/lib/build/recipe/openid/index.js b/lib/build/recipe/openid/index.js deleted file mode 100644 index 83903ab76..000000000 --- a/lib/build/recipe/openid/index.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getJWKS = exports.createJWT = exports.getOpenIdDiscoveryConfiguration = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -class OpenIdRecipeWrapper { - static getOpenIdDiscoveryConfiguration(userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ - userContext: userContext === undefined ? {} : userContext, - }); - } - static createJWT(payload, validitySeconds, userContext) { - return recipe_1.default.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.createJWT({ - payload, - validitySeconds, - userContext: userContext === undefined ? {} : userContext, - }); - } - static getJWKS(userContext) { - return recipe_1.default.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, - }); - } -} -exports.default = OpenIdRecipeWrapper; -OpenIdRecipeWrapper.init = recipe_1.default.init; -exports.init = OpenIdRecipeWrapper.init; -exports.getOpenIdDiscoveryConfiguration = OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration; -exports.createJWT = OpenIdRecipeWrapper.createJWT; -exports.getJWKS = OpenIdRecipeWrapper.getJWKS; diff --git a/lib/build/recipe/openid/recipe.d.ts b/lib/build/recipe/openid/recipe.d.ts deleted file mode 100644 index 0de7ecdc7..000000000 --- a/lib/build/recipe/openid/recipe.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import normalisedURLPath from "../../normalisedURLPath"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import JWTRecipe from "../jwt/recipe"; -export default class OpenIdRecipe extends RecipeModule { - static RECIPE_ID: string; - private static instance; - config: TypeNormalisedInput; - jwtRecipe: JWTRecipe; - recipeImplementation: RecipeInterface; - apiImpl: APIInterface; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); - static getInstanceOrThrowError(): OpenIdRecipe; - static init(config?: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - response: BaseResponse, - path: normalisedURLPath, - method: HTTPMethod - ) => Promise; - handleError: (error: STError, request: BaseRequest, response: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; -} diff --git a/lib/build/recipe/openid/recipe.js b/lib/build/recipe/openid/recipe.js deleted file mode 100644 index baf6bc665..000000000 --- a/lib/build/recipe/openid/recipe.js +++ /dev/null @@ -1,146 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const error_1 = __importDefault(require("../../error")); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const utils_1 = require("./utils"); -const recipe_1 = __importDefault(require("../jwt/recipe")); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const constants_1 = require("./constants"); -const getOpenIdDiscoveryConfiguration_1 = __importDefault(require("./api/getOpenIdDiscoveryConfiguration")); -class OpenIdRecipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config) { - super(recipeId, appInfo); - this.getAPIsHandled = () => { - return [ - { - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.GET_DISCOVERY_CONFIG_URL), - id: constants_1.GET_DISCOVERY_CONFIG_URL, - disabled: this.apiImpl.getOpenIdDiscoveryConfigurationGET === undefined, - }, - ...this.jwtRecipe.getAPIsHandled(), - ]; - }; - this.handleAPIRequest = (id, req, response, path, method) => - __awaiter(this, void 0, void 0, function* () { - let apiOptions = { - recipeImplementation: this.recipeImplementation, - config: this.config, - recipeId: this.getRecipeId(), - req, - res: response, - }; - if (id === constants_1.GET_DISCOVERY_CONFIG_URL) { - return yield getOpenIdDiscoveryConfiguration_1.default(this.apiImpl, apiOptions); - } else { - return this.jwtRecipe.handleAPIRequest(id, req, response, path, method); - } - }); - this.handleError = (error, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { - throw error; - } else { - return yield this.jwtRecipe.handleError(error, request, response); - } - }); - this.getAllCORSHeaders = () => { - return [...this.jwtRecipe.getAllCORSHeaders()]; - }; - this.isErrorFromThisRecipe = (err) => { - return ( - (error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === OpenIdRecipe.RECIPE_ID) || - this.jwtRecipe.isErrorFromThisRecipe(err) - ); - }; - this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); - this.jwtRecipe = new recipe_1.default(recipeId, appInfo, isInServerlessEnv, { - jwtValiditySeconds: this.config.jwtValiditySeconds, - override: this.config.override.jwtFeature, - }); - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(this.config, this.jwtRecipe.recipeInterfaceImpl) - ); - this.recipeImplementation = builder.override(this.config.override.functions).build(); - let apiBuilder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = apiBuilder.override(this.config.override.apis).build(); - } - static getInstanceOrThrowError() { - if (OpenIdRecipe.instance !== undefined) { - return OpenIdRecipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (OpenIdRecipe.instance === undefined) { - OpenIdRecipe.instance = new OpenIdRecipe(OpenIdRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return OpenIdRecipe.instance; - } else { - throw new Error("OpenId recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - OpenIdRecipe.instance = undefined; - } -} -exports.default = OpenIdRecipe; -OpenIdRecipe.RECIPE_ID = "openid"; -OpenIdRecipe.instance = undefined; diff --git a/lib/build/recipe/openid/recipeImplementation.d.ts b/lib/build/recipe/openid/recipeImplementation.d.ts deleted file mode 100644 index d4698099c..000000000 --- a/lib/build/recipe/openid/recipeImplementation.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { RecipeInterface, TypeNormalisedInput } from "./types"; -import { RecipeInterface as JWTRecipeInterface } from "../jwt/types"; -export default function getRecipeInterface( - config: TypeNormalisedInput, - jwtRecipeImplementation: JWTRecipeInterface -): RecipeInterface; diff --git a/lib/build/recipe/openid/recipeImplementation.js b/lib/build/recipe/openid/recipeImplementation.js deleted file mode 100644 index efa935aaa..000000000 --- a/lib/build/recipe/openid/recipeImplementation.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const constants_1 = require("../jwt/constants"); -function getRecipeInterface(config, jwtRecipeImplementation) { - return { - getOpenIdDiscoveryConfiguration: function () { - return __awaiter(this, void 0, void 0, function* () { - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - let jwks_uri = - config.issuerDomain.getAsStringDangerous() + - config.issuerPath - .appendPath(new normalisedURLPath_1.default(constants_1.GET_JWKS_API)) - .getAsStringDangerous(); - return { - status: "OK", - issuer, - jwks_uri, - }; - }); - }, - createJWT: function ({ payload, validitySeconds, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - payload = payload === undefined || payload === null ? {} : payload; - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - return yield jwtRecipeImplementation.createJWT({ - payload: Object.assign({ iss: issuer }, payload), - validitySeconds, - userContext, - }); - }); - }, - getJWKS: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield jwtRecipeImplementation.getJWKS(input); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/openid/types.d.ts b/lib/build/recipe/openid/types.d.ts deleted file mode 100644 index a480f4b98..000000000 --- a/lib/build/recipe/openid/types.d.ts +++ /dev/null @@ -1,100 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLDomain from "../../normalisedURLDomain"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; -import { GeneralErrorResponse } from "../../types"; -export declare type TypeInput = { - issuer?: string; - jwtValiditySeconds?: number; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; -}; -export declare type TypeNormalisedInput = { - issuerDomain: NormalisedURLDomain; - issuerPath: NormalisedURLPath; - jwtValiditySeconds?: number; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - req: BaseRequest; - res: BaseResponse; -}; -export declare type APIInterface = { - getOpenIdDiscoveryConfigurationGET: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - issuer: string; - jwks_uri: string; - } - | GeneralErrorResponse - >); -}; -export declare type RecipeInterface = { - getOpenIdDiscoveryConfiguration(input: { - userContext: any; - }): Promise<{ - status: "OK"; - issuer: string; - jwks_uri: string; - }>; - createJWT(input: { - payload?: any; - validitySeconds?: number; - userContext: any; - }): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - getJWKS(input: { - userContext: any; - }): Promise<{ - status: "OK"; - keys: JsonWebKey[]; - }>; -}; diff --git a/lib/build/recipe/openid/types.js b/lib/build/recipe/openid/types.js deleted file mode 100644 index c8ad2e549..000000000 --- a/lib/build/recipe/openid/types.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/openid/utils.d.ts b/lib/build/recipe/openid/utils.d.ts deleted file mode 100644 index 6b5abd280..000000000 --- a/lib/build/recipe/openid/utils.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; diff --git a/lib/build/recipe/openid/utils.js b/lib/build/recipe/openid/utils.js deleted file mode 100644 index fac3a3f43..000000000 --- a/lib/build/recipe/openid/utils.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const normalisedURLDomain_1 = __importDefault(require("../../normalisedURLDomain")); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function validateAndNormaliseUserInput(appInfo, config) { - let issuerDomain = appInfo.apiDomain; - let issuerPath = appInfo.apiBasePath; - if (config !== undefined) { - if (config.issuer !== undefined) { - issuerDomain = new normalisedURLDomain_1.default(config.issuer); - issuerPath = new normalisedURLPath_1.default(config.issuer); - } - if (!issuerPath.equals(appInfo.apiBasePath)) { - throw new Error("The path of the issuer URL must be equal to the apiBasePath. The default value is /auth"); - } - } - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - return { - issuerDomain, - issuerPath, - jwtValiditySeconds: config === null || config === void 0 ? void 0 : config.jwtValiditySeconds, - override, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/passwordless/api/consumeCode.d.ts b/lib/build/recipe/passwordless/api/consumeCode.d.ts deleted file mode 100644 index 0f21b8d73..000000000 --- a/lib/build/recipe/passwordless/api/consumeCode.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from ".."; -export default function consumeCode(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/passwordless/api/consumeCode.js b/lib/build/recipe/passwordless/api/consumeCode.js deleted file mode 100644 index 27cd79132..000000000 --- a/lib/build/recipe/passwordless/api/consumeCode.js +++ /dev/null @@ -1,115 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const utils_2 = require("../../../utils"); -function consumeCode(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.consumeCodePOST === undefined) { - return false; - } - const body = yield options.req.getJSONBody(); - const preAuthSessionId = body.preAuthSessionId; - const linkCode = body.linkCode; - const deviceId = body.deviceId; - const userInputCode = body.userInputCode; - if (preAuthSessionId === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide preAuthSessionId", - }); - } - if (deviceId !== undefined || userInputCode !== undefined) { - if (linkCode !== undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", - }); - } - if (deviceId === undefined || userInputCode === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide both deviceId and userInputCode", - }); - } - } else if (linkCode === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", - }); - } - const userContext = utils_2.makeDefaultUserContextFromAPI(options.req); - let result = yield apiImplementation.consumeCodePOST( - deviceId !== undefined - ? { - deviceId, - userInputCode, - preAuthSessionId, - options, - userContext, - } - : { - linkCode, - options, - preAuthSessionId, - userContext, - } - ); - if (result.status === "OK") { - delete result.session; - } - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = consumeCode; diff --git a/lib/build/recipe/passwordless/api/createCode.d.ts b/lib/build/recipe/passwordless/api/createCode.d.ts deleted file mode 100644 index d72ea96e0..000000000 --- a/lib/build/recipe/passwordless/api/createCode.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from ".."; -export default function createCode(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/passwordless/api/createCode.js b/lib/build/recipe/passwordless/api/createCode.js deleted file mode 100644 index 83e8388a8..000000000 --- a/lib/build/recipe/passwordless/api/createCode.js +++ /dev/null @@ -1,128 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const max_1 = __importDefault(require("libphonenumber-js/max")); -const utils_2 = require("../../../utils"); -function createCode(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.createCodePOST === undefined) { - return false; - } - const body = yield options.req.getJSONBody(); - let email = body.email; - let phoneNumber = body.phoneNumber; - if ((email !== undefined && phoneNumber !== undefined) || (email === undefined && phoneNumber === undefined)) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide exactly one of email or phoneNumber", - }); - } - if (email === undefined && options.config.contactMethod === "EMAIL") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: 'Please provide an email since you have set the contactMethod to "EMAIL"', - }); - } - if (phoneNumber === undefined && options.config.contactMethod === "PHONE") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: 'Please provide a phoneNumber since you have set the contactMethod to "PHONE"', - }); - } - // normalise and validate format of input - if ( - email !== undefined && - (options.config.contactMethod === "EMAIL" || options.config.contactMethod === "EMAIL_OR_PHONE") - ) { - email = email.trim(); - const validateError = yield options.config.validateEmailAddress(email); - if (validateError !== undefined) { - utils_1.send200Response(options.res, { - status: "GENERAL_ERROR", - message: validateError, - }); - return true; - } - } - if ( - phoneNumber !== undefined && - (options.config.contactMethod === "PHONE" || options.config.contactMethod === "EMAIL_OR_PHONE") - ) { - const validateError = yield options.config.validatePhoneNumber(phoneNumber); - if (validateError !== undefined) { - utils_1.send200Response(options.res, { - status: "GENERAL_ERROR", - message: validateError, - }); - return true; - } - const parsedPhoneNumber = max_1.default(phoneNumber); - if (parsedPhoneNumber === undefined) { - // this can come here if the user has provided their own impl of validatePhoneNumber and - // the phone number is valid according to their impl, but not according to the libphonenumber-js lib. - phoneNumber = phoneNumber.trim(); - } else { - phoneNumber = parsedPhoneNumber.format("E.164"); - } - } - let result = yield apiImplementation.createCodePOST( - email !== undefined - ? { email, options, userContext: utils_2.makeDefaultUserContextFromAPI(options.req) } - : { phoneNumber: phoneNumber, options, userContext: utils_2.makeDefaultUserContextFromAPI(options.req) } - ); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = createCode; diff --git a/lib/build/recipe/passwordless/api/emailExists.d.ts b/lib/build/recipe/passwordless/api/emailExists.d.ts deleted file mode 100644 index 74f301a87..000000000 --- a/lib/build/recipe/passwordless/api/emailExists.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function emailExists(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/passwordless/api/emailExists.js b/lib/build/recipe/passwordless/api/emailExists.js deleted file mode 100644 index e1fe058a4..000000000 --- a/lib/build/recipe/passwordless/api/emailExists.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const utils_2 = require("../../../utils"); -function emailExists(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.emailExistsGET === undefined) { - return false; - } - let email = options.req.getKeyValueFromQuery("email"); - if (email === undefined || typeof email !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the email as a GET param", - }); - } - let result = yield apiImplementation.emailExistsGET({ - email, - options, - userContext: utils_2.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = emailExists; diff --git a/lib/build/recipe/passwordless/api/implementation.d.ts b/lib/build/recipe/passwordless/api/implementation.d.ts deleted file mode 100644 index 402db9918..000000000 --- a/lib/build/recipe/passwordless/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../"; -export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/passwordless/api/implementation.js b/lib/build/recipe/passwordless/api/implementation.js deleted file mode 100644 index f7dcdb498..000000000 --- a/lib/build/recipe/passwordless/api/implementation.js +++ /dev/null @@ -1,297 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const logger_1 = require("../../../logger"); -const recipe_1 = __importDefault(require("../../emailverification/recipe")); -const session_1 = __importDefault(require("../../session")); -function getAPIImplementation() { - return { - consumeCodePOST: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.consumeCode( - "deviceId" in input - ? { - preAuthSessionId: input.preAuthSessionId, - deviceId: input.deviceId, - userInputCode: input.userInputCode, - userContext: input.userContext, - } - : { - preAuthSessionId: input.preAuthSessionId, - linkCode: input.linkCode, - userContext: input.userContext, - } - ); - if (response.status !== "OK") { - return response; - } - let user = response.user; - if (user.email !== undefined) { - const emailVerificationInstance = recipe_1.default.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = yield emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: user.id, - email: user.email, - userContext: input.userContext, - } - ); - if (tokenResponse.status === "OK") { - yield emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - userContext: input.userContext, - }); - } - } - } - const session = yield session_1.default.createNewSession( - input.options.req, - input.options.res, - user.id, - {}, - {}, - input.userContext - ); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - session, - }; - }); - }, - createCodePOST: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.createCode( - "email" in input - ? { - userContext: input.userContext, - email: input.email, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : yield input.options.config.getCustomUserInputCode(input.userContext), - } - : { - userContext: input.userContext, - phoneNumber: input.phoneNumber, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : yield input.options.config.getCustomUserInputCode(input.userContext), - } - ); - // now we send the email / text message. - let magicLink = undefined; - let userInputCode = undefined; - const flowType = input.options.config.flowType; - if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - magicLink = - input.options.appInfo.websiteDomain.getAsStringDangerous() + - input.options.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - input.options.recipeId + - "&preAuthSessionId=" + - response.preAuthSessionId + - "#" + - response.linkCode; - } - if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - userInputCode = response.userInputCode; - } - // we don't do something special for serverless env here - // cause we want to wait for service's reply since it can show - // a UI error message for if sending an SMS / email failed or not. - if ( - input.options.config.contactMethod === "PHONE" || - (input.options.config.contactMethod === "EMAIL_OR_PHONE" && "phoneNumber" in input) - ) { - logger_1.logDebugMessage(`Sending passwordless login SMS to ${input.phoneNumber}`); - yield input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ - type: "PASSWORDLESS_LOGIN", - codeLifetime: response.codeLifetime, - phoneNumber: input.phoneNumber, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } else { - logger_1.logDebugMessage(`Sending passwordless login email to ${input.email}`); - yield input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORDLESS_LOGIN", - email: input.email, - codeLifetime: response.codeLifetime, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } - return { - status: "OK", - deviceId: response.deviceId, - flowType: input.options.config.flowType, - preAuthSessionId: response.preAuthSessionId, - }; - }); - }, - emailExistsGET: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.getUserByEmail({ - userContext: input.userContext, - email: input.email, - }); - return { - exists: response !== undefined, - status: "OK", - }; - }); - }, - phoneNumberExistsGET: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield input.options.recipeImplementation.getUserByPhoneNumber({ - userContext: input.userContext, - phoneNumber: input.phoneNumber, - }); - return { - exists: response !== undefined, - status: "OK", - }; - }); - }, - resendCodePOST: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let deviceInfo = yield input.options.recipeImplementation.listCodesByDeviceId({ - userContext: input.userContext, - deviceId: input.deviceId, - }); - if (deviceInfo === undefined) { - return { - status: "RESTART_FLOW_ERROR", - }; - } - if ( - (input.options.config.contactMethod === "PHONE" && deviceInfo.phoneNumber === undefined) || - (input.options.config.contactMethod === "EMAIL" && deviceInfo.email === undefined) - ) { - return { - status: "RESTART_FLOW_ERROR", - }; - } - let numberOfTriesToCreateNewCode = 0; - while (true) { - numberOfTriesToCreateNewCode++; - let response = yield input.options.recipeImplementation.createNewCodeForDevice({ - userContext: input.userContext, - deviceId: input.deviceId, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : yield input.options.config.getCustomUserInputCode(input.userContext), - }); - if (response.status === "USER_INPUT_CODE_ALREADY_USED_ERROR") { - if (numberOfTriesToCreateNewCode >= 3) { - // we retry 3 times. - return { - status: "GENERAL_ERROR", - message: "Failed to generate a one time code. Please try again", - }; - } - continue; - } - if (response.status === "OK") { - let magicLink = undefined; - let userInputCode = undefined; - const flowType = input.options.config.flowType; - if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - magicLink = - input.options.appInfo.websiteDomain.getAsStringDangerous() + - input.options.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - input.options.recipeId + - "&preAuthSessionId=" + - response.preAuthSessionId + - "#" + - response.linkCode; - } - if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - userInputCode = response.userInputCode; - } - // we don't do something special for serverless env here - // cause we want to wait for service's reply since it can show - // a UI error message for if sending an SMS / email failed or not. - if ( - input.options.config.contactMethod === "PHONE" || - (input.options.config.contactMethod === "EMAIL_OR_PHONE" && - deviceInfo.phoneNumber !== undefined) - ) { - logger_1.logDebugMessage(`Sending passwordless login SMS to ${input.phoneNumber}`); - yield input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ - type: "PASSWORDLESS_LOGIN", - codeLifetime: response.codeLifetime, - phoneNumber: deviceInfo.phoneNumber, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } else { - logger_1.logDebugMessage(`Sending passwordless login email to ${input.email}`); - yield input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORDLESS_LOGIN", - email: deviceInfo.email, - codeLifetime: response.codeLifetime, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } - } - return { - status: response.status, - }; - } - }); - }, - }; -} -exports.default = getAPIImplementation; diff --git a/lib/build/recipe/passwordless/api/phoneNumberExists.d.ts b/lib/build/recipe/passwordless/api/phoneNumberExists.d.ts deleted file mode 100644 index 9416f0cda..000000000 --- a/lib/build/recipe/passwordless/api/phoneNumberExists.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from ".."; -export default function phoneNumberExists(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/passwordless/api/phoneNumberExists.js b/lib/build/recipe/passwordless/api/phoneNumberExists.js deleted file mode 100644 index 64db5374f..000000000 --- a/lib/build/recipe/passwordless/api/phoneNumberExists.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const utils_2 = require("../../../utils"); -function phoneNumberExists(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.phoneNumberExistsGET === undefined) { - return false; - } - let phoneNumber = options.req.getKeyValueFromQuery("phoneNumber"); - if (phoneNumber === undefined || typeof phoneNumber !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the phoneNumber as a GET param", - }); - } - let result = yield apiImplementation.phoneNumberExistsGET({ - phoneNumber, - options, - userContext: utils_2.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = phoneNumberExists; diff --git a/lib/build/recipe/passwordless/api/resendCode.d.ts b/lib/build/recipe/passwordless/api/resendCode.d.ts deleted file mode 100644 index ad4629bb6..000000000 --- a/lib/build/recipe/passwordless/api/resendCode.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from ".."; -export default function resendCode(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/passwordless/api/resendCode.js b/lib/build/recipe/passwordless/api/resendCode.js deleted file mode 100644 index 9fad206c2..000000000 --- a/lib/build/recipe/passwordless/api/resendCode.js +++ /dev/null @@ -1,86 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const utils_2 = require("../../../utils"); -function resendCode(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.resendCodePOST === undefined) { - return false; - } - const body = yield options.req.getJSONBody(); - const preAuthSessionId = body.preAuthSessionId; - const deviceId = body.deviceId; - if (preAuthSessionId === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide preAuthSessionId", - }); - } - if (deviceId === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide a deviceId", - }); - } - let result = yield apiImplementation.resendCodePOST({ - deviceId, - preAuthSessionId, - options, - userContext: utils_2.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = resendCode; diff --git a/lib/build/recipe/passwordless/constants.d.ts b/lib/build/recipe/passwordless/constants.d.ts deleted file mode 100644 index f7438a00e..000000000 --- a/lib/build/recipe/passwordless/constants.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -export declare const CREATE_CODE_API = "/signinup/code"; -export declare const RESEND_CODE_API = "/signinup/code/resend"; -export declare const CONSUME_CODE_API = "/signinup/code/consume"; -export declare const DOES_EMAIL_EXIST_API = "/signup/email/exists"; -export declare const DOES_PHONE_NUMBER_EXIST_API = "/signup/phonenumber/exists"; diff --git a/lib/build/recipe/passwordless/constants.js b/lib/build/recipe/passwordless/constants.js deleted file mode 100644 index cd5590dc1..000000000 --- a/lib/build/recipe/passwordless/constants.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DOES_PHONE_NUMBER_EXIST_API = exports.DOES_EMAIL_EXIST_API = exports.CONSUME_CODE_API = exports.RESEND_CODE_API = exports.CREATE_CODE_API = void 0; -exports.CREATE_CODE_API = "/signinup/code"; -exports.RESEND_CODE_API = "/signinup/code/resend"; -exports.CONSUME_CODE_API = "/signinup/code/consume"; -exports.DOES_EMAIL_EXIST_API = "/signup/email/exists"; -exports.DOES_PHONE_NUMBER_EXIST_API = "/signup/phonenumber/exists"; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.d.ts deleted file mode 100644 index 2f0015dda..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { NormalisedAppinfo } from "../../../../../types"; -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private createAndSendCustomEmail; - constructor( - appInfo: NormalisedAppinfo, - createAndSendCustomEmail?: ( - input: { - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise - ); - sendEmail: ( - input: TypePasswordlessEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.js deleted file mode 100644 index 241629b1a..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.js +++ /dev/null @@ -1,124 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -const logger_1 = require("../../../../../logger"); -function defaultCreateAndSendCustomEmail(appInfo) { - return (input, _) => - __awaiter(this, void 0, void 0, function* () { - if (process.env.TEST_MODE === "testing") { - return; - } - try { - yield axios_1.default({ - method: "POST", - url: "https://api.supertokens.io/0/st/auth/passwordless/login", - data: { - email: input.email, - appName: appInfo.appName, - codeLifetime: input.codeLifetime, - urlWithLinkCode: input.urlWithLinkCode, - userInputCode: input.userInputCode, - }, - headers: { - "api-version": 0, - }, - }); - logger_1.logDebugMessage(`Email sent to ${input.email}`); - } catch (error) { - logger_1.logDebugMessage("Error sending passwordless login email"); - if (axios_1.default.isAxiosError(error)) { - const err = error; - if (err.response) { - logger_1.logDebugMessage(`Error status: ${err.response.status}`); - logger_1.logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logger_1.logDebugMessage(`Error: ${err.message}`); - } - } else { - logger_1.logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logger_1.logDebugMessage("Logging the input below:"); - logger_1.logDebugMessage( - JSON.stringify( - { - email: input.email, - appName: appInfo.appName, - codeLifetime: input.codeLifetime, - urlWithLinkCode: input.urlWithLinkCode, - userInputCode: input.userInputCode, - }, - null, - 2 - ) - ); - /** - * if the error is thrown from API, the response object - * will be of type `{err: string}` - */ - if (axios_1.default.isAxiosError(error) && error.response !== undefined) { - if (error.response.data.err !== undefined) { - throw Error(error.response.data.err); - } - } - throw error; - } - }); -} -class BackwardCompatibilityService { - constructor(appInfo, createAndSendCustomEmail) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.createAndSendCustomEmail( - { - email: input.email, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - preAuthSessionId: input.preAuthSessionId, - codeLifetime: input.codeLifetime, - }, - input.userContext - ); - }); - this.createAndSendCustomEmail = - createAndSendCustomEmail === undefined - ? defaultCreateAndSendCustomEmail(appInfo) - : createAndSendCustomEmail; - } -} -exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/index.d.ts b/lib/build/recipe/passwordless/emaildelivery/services/index.d.ts deleted file mode 100644 index 4de04d983..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import SMTP from "./smtp"; -export declare let SMTPService: typeof SMTP; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/index.js b/lib/build/recipe/passwordless/emaildelivery/services/index.js deleted file mode 100644 index 7e07f6706..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/index.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SMTPService = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const smtp_1 = __importDefault(require("./smtp")); -exports.SMTPService = smtp_1.default; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.d.ts deleted file mode 100644 index 3cda03b71..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - constructor(config: TypeInput); - sendEmail: ( - input: TypePasswordlessEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.js b/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.js deleted file mode 100644 index e917fb0fe..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const nodemailer_1 = require("nodemailer"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const serviceImplementation_1 = require("./serviceImplementation"); -class SMTPService { - constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - yield this.serviceImpl.sendRawEmail( - Object.assign(Object.assign({}, content), { userContext: input.userContext }) - ); - }); - const transporter = nodemailer_1.createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new supertokens_js_override_1.default( - serviceImplementation_1.getServiceImplementation(transporter, config.smtpSettings.from) - ); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } -} -exports.default = SMTPService; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.d.ts b/lib/build/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.d.ts deleted file mode 100644 index e3ae65d56..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/emaildelivery/services/smtp"; -export default function getPasswordlessLoginEmailContent(input: TypePasswordlessEmailDeliveryInput): GetContentResult; -export declare function getPasswordlessLoginEmailHTML( - appName: string, - email: string, - codeLifetime: number, - urlWithLinkCode?: string, - userInputCode?: string -): string; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.js b/lib/build/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.js deleted file mode 100644 index 1bc7a41ba..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.js +++ /dev/null @@ -1,2864 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getPasswordlessLoginEmailHTML = void 0; -const supertokens_1 = __importDefault(require("../../../../../supertokens")); -const utils_1 = require("../../../../../utils"); -function getPasswordlessLoginEmailContent(input) { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getPasswordlessLoginEmailHTML( - appName, - input.email, - input.codeLifetime, - input.urlWithLinkCode, - input.userInputCode - ); - return { - body, - toEmail: input.email, - subject: "Login to your account", - isHtml: true, - }; -} -exports.default = getPasswordlessLoginEmailContent; -function getPasswordlessLoginOTPBody(appName, email, codeLifetime, userInputCode) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
-

- Login to ${appName}

-
- - - - - - -
- - -
-
- -

- Enter the below OTP in your login screen. Note - that the OTP expires in ${codeLifetime}.

- -
-
- ${userInputCode}
- -
-
-
-
- - - - - - -
- - -

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} -function getPasswordlessLoginURLLinkBody(appName, email, codeLifetime, urlWithLinkCode) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
-

- Login to ${appName}

-
- - - - - - -
- - -
-
- -

- Please click the button below to sign in / up. - Note that the link expires in ${codeLifetime}. -

- -
- Login -
-
-
-

- Alternatively, you can directly paste this link - in your browser
- ${urlWithLinkCode} -

-
-
- - - - -
- - - - - - -
- - -

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} -function getPasswordlessLoginOTPAndURLLinkBody(appName, email, codeLifetime, urlWithLinkCode, userInputCode) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
-

- Login to ${appName}

-
- - - - - - -
-
-
-

- Enter the below OTP in your login screen. Note - that the OTP expires in ${codeLifetime}.

-
- -
-
- ${userInputCode}
-
-
-
- - - - - - -
- - - - - - - - - - -
- - or -
- - - -
- - - - - - -
- - -
-
- -

- Please click the button below to sign in / up. - Note that the link expires in ${codeLifetime}.

- -
- Login -
-
- -
-

- Alternatively, you can directly paste this link - in your browser
- ${urlWithLinkCode} -

-
-
- - -
- - - - - - -
-

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} -function getPasswordlessLoginEmailHTML(appName, email, codeLifetime, urlWithLinkCode, userInputCode) { - if (urlWithLinkCode !== undefined && userInputCode !== undefined) { - return getPasswordlessLoginOTPAndURLLinkBody( - appName, - email, - utils_1.humaniseMilliseconds(codeLifetime), - urlWithLinkCode, - userInputCode - ); - } - if (userInputCode !== undefined) { - return getPasswordlessLoginOTPBody(appName, email, utils_1.humaniseMilliseconds(codeLifetime), userInputCode); - } - if (urlWithLinkCode !== undefined) { - return getPasswordlessLoginURLLinkBody( - appName, - email, - utils_1.humaniseMilliseconds(codeLifetime), - urlWithLinkCode - ); - } - throw Error("this should never be thrown"); -} -exports.getPasswordlessLoginEmailHTML = getPasswordlessLoginEmailHTML; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.d.ts b/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.d.ts deleted file mode 100644 index 7a58ac4e4..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -import { Transporter } from "nodemailer"; -import { ServiceInterface } from "../../../../../ingredients/emaildelivery/services/smtp"; -export declare function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.js b/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.js deleted file mode 100644 index dfffb9e5d..000000000 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getServiceImplementation = void 0; -const passwordlessLogin_1 = __importDefault(require("./passwordlessLogin")); -function getServiceImplementation(transporter, from) { - return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (input.isHtml) { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }); - }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return passwordlessLogin_1.default(input); - }); - }, - }; -} -exports.getServiceImplementation = getServiceImplementation; diff --git a/lib/build/recipe/passwordless/error.d.ts b/lib/build/recipe/passwordless/error.d.ts deleted file mode 100644 index 486758b61..000000000 --- a/lib/build/recipe/passwordless/error.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -export default class SessionError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }); -} diff --git a/lib/build/recipe/passwordless/error.js b/lib/build/recipe/passwordless/error.js deleted file mode 100644 index 852278b6d..000000000 --- a/lib/build/recipe/passwordless/error.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class SessionError extends error_1.default { - constructor(options) { - super(Object.assign({}, options)); - this.fromRecipe = "passwordless"; - } -} -exports.default = SessionError; diff --git a/lib/build/recipe/passwordless/index.d.ts b/lib/build/recipe/passwordless/index.d.ts deleted file mode 100644 index f8966f784..000000000 --- a/lib/build/recipe/passwordless/index.d.ts +++ /dev/null @@ -1,183 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { - RecipeInterface, - User, - APIOptions, - APIInterface, - TypePasswordlessEmailDeliveryInput, - TypePasswordlessSmsDeliveryInput, -} from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static Error: typeof SuperTokensError; - static createCode( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { - userInputCode?: string; - userContext?: any; - } - ): Promise<{ - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - }>; - static createNewCodeForDevice(input: { - deviceId: string; - userInputCode?: string; - userContext?: any; - }): Promise< - | { - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - } - | { - status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR"; - } - >; - static consumeCode( - input: - | { - preAuthSessionId: string; - userInputCode: string; - deviceId: string; - userContext?: any; - } - | { - preAuthSessionId: string; - linkCode: string; - userContext?: any; - } - ): Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | { - status: "RESTART_FLOW_ERROR"; - } - >; - static getUserById(input: { userId: string; userContext?: any }): Promise; - static getUserByEmail(input: { email: string; userContext?: any }): Promise; - static getUserByPhoneNumber(input: { phoneNumber: string; userContext?: any }): Promise; - static updateUser(input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext?: any; - }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; - static revokeAllCodes( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ): Promise<{ - status: "OK"; - }>; - static revokeCode(input: { - codeId: string; - userContext?: any; - }): Promise<{ - status: "OK"; - }>; - static listCodesByEmail(input: { email: string; userContext?: any }): Promise; - static listCodesByPhoneNumber(input: { - phoneNumber: string; - userContext?: any; - }): Promise; - static listCodesByDeviceId(input: { - deviceId: string; - userContext?: any; - }): Promise; - static listCodesByPreAuthSessionId(input: { - preAuthSessionId: string; - userContext?: any; - }): Promise; - static createMagicLink( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ): Promise; - static signInUp( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ): Promise<{ - status: string; - createdNewUser: boolean; - user: User; - }>; - static sendEmail( - input: TypePasswordlessEmailDeliveryInput & { - userContext?: any; - } - ): Promise; - static sendSms( - input: TypePasswordlessSmsDeliveryInput & { - userContext?: any; - } - ): Promise; -} -export declare let init: typeof Recipe.init; -export declare let Error: typeof SuperTokensError; -export declare let createCode: typeof Wrapper.createCode; -export declare let consumeCode: typeof Wrapper.consumeCode; -export declare let getUserByEmail: typeof Wrapper.getUserByEmail; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByPhoneNumber: typeof Wrapper.getUserByPhoneNumber; -export declare let listCodesByDeviceId: typeof Wrapper.listCodesByDeviceId; -export declare let listCodesByEmail: typeof Wrapper.listCodesByEmail; -export declare let listCodesByPhoneNumber: typeof Wrapper.listCodesByPhoneNumber; -export declare let listCodesByPreAuthSessionId: typeof Wrapper.listCodesByPreAuthSessionId; -export declare let createNewCodeForDevice: typeof Wrapper.createNewCodeForDevice; -export declare let updateUser: typeof Wrapper.updateUser; -export declare let revokeAllCodes: typeof Wrapper.revokeAllCodes; -export declare let revokeCode: typeof Wrapper.revokeCode; -export declare let createMagicLink: typeof Wrapper.createMagicLink; -export declare let signInUp: typeof Wrapper.signInUp; -export type { RecipeInterface, User, APIOptions, APIInterface }; -export declare let sendEmail: typeof Wrapper.sendEmail; -export declare let sendSms: typeof Wrapper.sendSms; diff --git a/lib/build/recipe/passwordless/index.js b/lib/build/recipe/passwordless/index.js deleted file mode 100644 index 4b86a87b7..000000000 --- a/lib/build/recipe/passwordless/index.js +++ /dev/null @@ -1,164 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendSms = exports.sendEmail = exports.signInUp = exports.createMagicLink = exports.revokeCode = exports.revokeAllCodes = exports.updateUser = exports.createNewCodeForDevice = exports.listCodesByPreAuthSessionId = exports.listCodesByPhoneNumber = exports.listCodesByEmail = exports.listCodesByDeviceId = exports.getUserByPhoneNumber = exports.getUserById = exports.getUserByEmail = exports.consumeCode = exports.createCode = exports.Error = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const error_1 = __importDefault(require("./error")); -class Wrapper { - static createCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createCode(Object.assign({ userContext: {} }, input)); - } - static createNewCodeForDevice(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createNewCodeForDevice(Object.assign({ userContext: {} }, input)); - } - static consumeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.consumeCode(Object.assign({ userContext: {} }, input)); - } - static getUserById(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserById(Object.assign({ userContext: {} }, input)); - } - static getUserByEmail(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserByEmail(Object.assign({ userContext: {} }, input)); - } - static getUserByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserByPhoneNumber(Object.assign({ userContext: {} }, input)); - } - static updateUser(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.updateUser(Object.assign({ userContext: {} }, input)); - } - static revokeAllCodes(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeAllCodes(Object.assign({ userContext: {} }, input)); - } - static revokeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeCode(Object.assign({ userContext: {} }, input)); - } - static listCodesByEmail(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByEmail(Object.assign({ userContext: {} }, input)); - } - static listCodesByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPhoneNumber(Object.assign({ userContext: {} }, input)); - } - static listCodesByDeviceId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByDeviceId(Object.assign({ userContext: {} }, input)); - } - static listCodesByPreAuthSessionId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPreAuthSessionId(Object.assign({ userContext: {} }, input)); - } - static createMagicLink(input) { - return recipe_1.default.getInstanceOrThrowError().createMagicLink(Object.assign({ userContext: {} }, input)); - } - static signInUp(input) { - return recipe_1.default.getInstanceOrThrowError().signInUp(Object.assign({ userContext: {} }, input)); - } - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .emailDelivery.ingredientInterfaceImpl.sendEmail(Object.assign({ userContext: {} }, input)); - }); - } - static sendSms(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .smsDelivery.ingredientInterfaceImpl.sendSms(Object.assign({ userContext: {} }, input)); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -Wrapper.Error = error_1.default; -exports.init = Wrapper.init; -exports.Error = Wrapper.Error; -exports.createCode = Wrapper.createCode; -exports.consumeCode = Wrapper.consumeCode; -exports.getUserByEmail = Wrapper.getUserByEmail; -exports.getUserById = Wrapper.getUserById; -exports.getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; -exports.listCodesByDeviceId = Wrapper.listCodesByDeviceId; -exports.listCodesByEmail = Wrapper.listCodesByEmail; -exports.listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber; -exports.listCodesByPreAuthSessionId = Wrapper.listCodesByPreAuthSessionId; -exports.createNewCodeForDevice = Wrapper.createNewCodeForDevice; -exports.updateUser = Wrapper.updateUser; -exports.revokeAllCodes = Wrapper.revokeAllCodes; -exports.revokeCode = Wrapper.revokeCode; -exports.createMagicLink = Wrapper.createMagicLink; -exports.signInUp = Wrapper.signInUp; -exports.sendEmail = Wrapper.sendEmail; -exports.sendSms = Wrapper.sendSms; diff --git a/lib/build/recipe/passwordless/recipe.d.ts b/lib/build/recipe/passwordless/recipe.d.ts deleted file mode 100644 index 42b76df0a..000000000 --- a/lib/build/recipe/passwordless/recipe.d.ts +++ /dev/null @@ -1,72 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import STError from "./error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput } from "./types"; -import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - emailDelivery: EmailDeliveryIngredient; - smsDelivery: SmsDeliveryIngredient; - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - smsDelivery: SmsDeliveryIngredient | undefined; - } - ); - static getInstanceOrThrowError(): Recipe; - static init(config: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - _: NormalisedURLPath, - __: HTTPMethod - ) => Promise; - handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; - createMagicLink: ( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) => Promise; - signInUp: ( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) => Promise<{ - status: string; - createdNewUser: boolean; - user: import("./types").User; - }>; - getEmailForUserId: GetEmailForUserIdFunc; -} diff --git a/lib/build/recipe/passwordless/recipe.js b/lib/build/recipe/passwordless/recipe.js deleted file mode 100644 index 5f3720526..000000000 --- a/lib/build/recipe/passwordless/recipe.js +++ /dev/null @@ -1,292 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const error_1 = __importDefault(require("./error")); -const utils_1 = require("./utils"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const recipe_1 = __importDefault(require("../emailverification/recipe")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const querier_1 = require("../../querier"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const consumeCode_1 = __importDefault(require("./api/consumeCode")); -const createCode_1 = __importDefault(require("./api/createCode")); -const emailExists_1 = __importDefault(require("./api/emailExists")); -const phoneNumberExists_1 = __importDefault(require("./api/phoneNumberExists")); -const resendCode_1 = __importDefault(require("./api/resendCode")); -const constants_1 = require("./constants"); -const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); -const smsdelivery_1 = __importDefault(require("../../ingredients/smsdelivery")); -const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { - super(recipeId, appInfo); - // abstract instance functions below............... - this.getAPIsHandled = () => { - return [ - { - id: constants_1.CONSUME_CODE_API, - disabled: this.apiImpl.consumeCodePOST === undefined, - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.CONSUME_CODE_API), - }, - { - id: constants_1.CREATE_CODE_API, - disabled: this.apiImpl.createCodePOST === undefined, - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.CREATE_CODE_API), - }, - { - id: constants_1.DOES_EMAIL_EXIST_API, - disabled: this.apiImpl.emailExistsGET === undefined, - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.DOES_EMAIL_EXIST_API), - }, - { - id: constants_1.DOES_PHONE_NUMBER_EXIST_API, - disabled: this.apiImpl.phoneNumberExistsGET === undefined, - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.DOES_PHONE_NUMBER_EXIST_API), - }, - { - id: constants_1.RESEND_CODE_API, - disabled: this.apiImpl.resendCodePOST === undefined, - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.RESEND_CODE_API), - }, - ]; - }; - this.handleAPIRequest = (id, req, res, _, __) => - __awaiter(this, void 0, void 0, function* () { - const options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - smsDelivery: this.smsDelivery, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.CONSUME_CODE_API) { - return yield consumeCode_1.default(this.apiImpl, options); - } else if (id === constants_1.CREATE_CODE_API) { - return yield createCode_1.default(this.apiImpl, options); - } else if (id === constants_1.DOES_EMAIL_EXIST_API) { - return yield emailExists_1.default(this.apiImpl, options); - } else if (id === constants_1.DOES_PHONE_NUMBER_EXIST_API) { - return yield phoneNumberExists_1.default(this.apiImpl, options); - } else { - return yield resendCode_1.default(this.apiImpl, options); - } - }); - this.handleError = (err, _, __) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); - this.getAllCORSHeaders = () => { - return []; - }; - this.isErrorFromThisRecipe = (err) => { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - // helper functions below... - this.createMagicLink = (input) => - __awaiter(this, void 0, void 0, function* () { - let userInputCode = - this.config.getCustomUserInputCode !== undefined - ? yield this.config.getCustomUserInputCode(input.userContext) - : undefined; - const codeInfo = yield this.recipeInterfaceImpl.createCode( - "email" in input - ? { - email: input.email, - userInputCode, - userContext: input.userContext, - } - : { - phoneNumber: input.phoneNumber, - userInputCode, - userContext: input.userContext, - } - ); - const appInfo = this.getAppInfo(); - let magicLink = - appInfo.websiteDomain.getAsStringDangerous() + - appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - this.getRecipeId() + - "&preAuthSessionId=" + - codeInfo.preAuthSessionId + - "#" + - codeInfo.linkCode; - return magicLink; - }); - this.signInUp = (input) => - __awaiter(this, void 0, void 0, function* () { - let codeInfo = yield this.recipeInterfaceImpl.createCode( - "email" in input - ? { - email: input.email, - userContext: input.userContext, - } - : { - phoneNumber: input.phoneNumber, - userContext: input.userContext, - } - ); - let consumeCodeResponse = yield this.recipeInterfaceImpl.consumeCode( - this.config.flowType === "MAGIC_LINK" - ? { - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - userContext: input.userContext, - } - : { - preAuthSessionId: codeInfo.preAuthSessionId, - deviceId: codeInfo.deviceId, - userInputCode: codeInfo.userInputCode, - userContext: input.userContext, - } - ); - if (consumeCodeResponse.status === "OK") { - return { - status: "OK", - createdNewUser: consumeCodeResponse.createdNewUser, - user: consumeCodeResponse.user, - }; - } else { - throw new Error("Failed to create user. Please retry"); - } - }); - // helper functions... - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - let userInfo = yield this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - if (userInfo.email !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "EMAIL_DOES_NOT_EXIST_ERROR", - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }); - this.isInServerlessEnv = isInServerlessEnv; - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new emaildelivery_1.default(this.config.getEmailDeliveryConfig()) - : ingredients.emailDelivery; - this.smsDelivery = - ingredients.smsDelivery === undefined - ? new smsdelivery_1.default(this.config.getSmsDeliveryConfig()) - : ingredients.smsDelivery; - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = recipe_1.default.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { - emailDelivery: undefined, - smsDelivery: undefined, - }); - return Recipe.instance; - } else { - throw new Error("Passwordless recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "passwordless"; diff --git a/lib/build/recipe/passwordless/recipeImplementation.d.ts b/lib/build/recipe/passwordless/recipeImplementation.d.ts deleted file mode 100644 index 86bf78a27..000000000 --- a/lib/build/recipe/passwordless/recipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "./types"; -import { Querier } from "../../querier"; -export default function getRecipeInterface(querier: Querier): RecipeInterface; diff --git a/lib/build/recipe/passwordless/recipeImplementation.js b/lib/build/recipe/passwordless/recipeImplementation.js deleted file mode 100644 index 50296548b..000000000 --- a/lib/build/recipe/passwordless/recipeImplementation.js +++ /dev/null @@ -1,177 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier) { - function copyAndRemoveUserContext(input) { - let result = Object.assign({}, input); - delete result.userContext; - return result; - } - return { - consumeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/signinup/code/consume"), - copyAndRemoveUserContext(input) - ); - return response; - }); - }, - createCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/signinup/code"), - copyAndRemoveUserContext(input) - ); - return response; - }); - }, - createNewCodeForDevice: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/signinup/code"), - copyAndRemoveUserContext(input) - ); - return response; - }); - }, - getUserByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/user"), - copyAndRemoveUserContext(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/user"), - copyAndRemoveUserContext(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }); - }, - getUserByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/user"), - copyAndRemoveUserContext(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }); - }, - listCodesByDeviceId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices.length === 1 ? response.devices[0] : undefined; - }); - }, - listCodesByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices; - }); - }, - listCodesByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices; - }); - }, - listCodesByPreAuthSessionId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices.length === 1 ? response.devices[0] : undefined; - }); - }, - revokeAllCodes: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/signinup/codes/remove"), - copyAndRemoveUserContext(input) - ); - return { - status: "OK", - }; - }); - }, - revokeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/signinup/code/remove"), - copyAndRemoveUserContext(input) - ); - return { status: "OK" }; - }); - }, - updateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPutRequest( - new normalisedURLPath_1.default("/recipe/user"), - copyAndRemoveUserContext(input) - ); - return response; - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.d.ts deleted file mode 100644 index d1477c878..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -// @ts-nocheck -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { NormalisedAppinfo } from "../../../../../types"; -export default class BackwardCompatibilityService implements SmsDeliveryInterface { - private createAndSendCustomSms; - constructor( - appInfo: NormalisedAppinfo, - createAndSendCustomSms?: ( - input: { - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise - ); - sendSms: ( - input: TypePasswordlessSmsDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.js b/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.js deleted file mode 100644 index 887b67227..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.js +++ /dev/null @@ -1,148 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -const supertokens_1 = require("../../../../../ingredients/smsdelivery/services/supertokens"); -const supertokens_2 = __importDefault(require("../../../../../supertokens")); -const logger_1 = require("../../../../../logger"); -function defaultCreateAndSendCustomSms(_) { - return (input, _) => - __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_2.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - try { - yield axios_1.default({ - method: "post", - url: supertokens_1.SUPERTOKENS_SMS_SERVICE_URL, - data: { - smsInput: { - appName, - type: "PASSWORDLESS_LOGIN", - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - }, - }, - headers: { - "api-version": "0", - }, - }); - logger_1.logDebugMessage(`Passwordless login SMS sent to ${input.phoneNumber}`); - return; - } catch (error) { - logger_1.logDebugMessage("Error sending passwordless login SMS"); - if (axios_1.default.isAxiosError(error)) { - const err = error; - if (err.response) { - logger_1.logDebugMessage(`Error status: ${err.response.status}`); - logger_1.logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logger_1.logDebugMessage(`Error: ${err.message}`); - } - if (err.response) { - if (err.response.status !== 429) { - /** - * if the error is thrown from API, the response object - * will be of type `{err: string}` - */ - if (err.response.data.err !== undefined) { - throw Error(err.response.data.err); - } else { - throw err; - } - } - } else { - throw err; - } - } else { - logger_1.logDebugMessage(`Error: ${JSON.stringify(error)}`); - throw error; - } - } - console.log( - "Free daily SMS quota reached. If you want to use SuperTokens to send SMS, please sign up on supertokens.com to get your SMS API key, else you can also define your own method by overriding the service. For now, we are logging it below:" - ); - /** - * if we do console.log(`SMS content: ${input}`); - * Output would be: - * SMS content: [object Object] - */ - /** - * JSON.stringify takes 3 inputs - * - value: usually an object or array, to be converted - * - replacer: An array of strings and numbers that acts - * as an approved list for selecting the object - * properties that will be stringified - * - space: Adds indentation, white space, and line break characters - * to the return-value JSON text to make it easier to read - * - * console.log(JSON.stringify({"a": 1, "b": 2})) - * Output: - * {"a":1,"b":2} - * - * console.log(JSON.stringify({"a": 1, "b": 2}, null, 2)) - * Output: - * { - * "a": 1, - * "b": 2 - * } - */ - console.log(`\nSMS content: ${JSON.stringify(input, null, 2)}`); - }); -} -class BackwardCompatibilityService { - constructor(appInfo, createAndSendCustomSms) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.createAndSendCustomSms( - { - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - preAuthSessionId: input.preAuthSessionId, - codeLifetime: input.codeLifetime, - }, - input.userContext - ); - }); - this.createAndSendCustomSms = - createAndSendCustomSms === undefined ? defaultCreateAndSendCustomSms(appInfo) : createAndSendCustomSms; - } -} -exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/index.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/index.d.ts deleted file mode 100644 index f14aacf83..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import Twilio from "./twilio"; -import Supertokens from "./supertokens"; -export declare let TwilioService: typeof Twilio; -export declare let SupertokensService: typeof Supertokens; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/index.js b/lib/build/recipe/passwordless/smsdelivery/services/index.js deleted file mode 100644 index f85fb8900..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/index.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SupertokensService = exports.TwilioService = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const twilio_1 = __importDefault(require("./twilio")); -const supertokens_1 = __importDefault(require("./supertokens")); -exports.TwilioService = twilio_1.default; -exports.SupertokensService = supertokens_1.default; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.d.ts deleted file mode 100644 index 501ecbce0..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -export default class SupertokensService implements SmsDeliveryInterface { - private apiKey; - constructor(apiKey: string); - sendSms: (input: TypePasswordlessSmsDeliveryInput) => Promise; -} diff --git a/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.js b/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.js deleted file mode 100644 index 712ce00bd..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/supertokens/index.js +++ /dev/null @@ -1,116 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const supertokens_1 = require("../../../../../ingredients/smsdelivery/services/supertokens"); -const axios_1 = __importDefault(require("axios")); -const supertokens_2 = __importDefault(require("../../../../../supertokens")); -const logger_1 = require("../../../../../logger"); -class SupertokensService { - constructor(apiKey) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_2.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - try { - yield axios_1.default({ - method: "post", - url: supertokens_1.SUPERTOKENS_SMS_SERVICE_URL, - data: { - apiKey: this.apiKey, - smsInput: { - type: input.type, - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - appName, - }, - }, - headers: { - "api-version": "0", - }, - }); - } catch (error) { - logger_1.logDebugMessage("Error sending SMS"); - if (axios_1.default.isAxiosError(error)) { - const err = error; - if (err.response) { - logger_1.logDebugMessage(`Error status: ${err.response.status}`); - logger_1.logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logger_1.logDebugMessage(`Error: ${err.message}`); - } - } else { - logger_1.logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logger_1.logDebugMessage("Logging the input below:"); - logger_1.logDebugMessage( - JSON.stringify( - { - type: input.type, - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - appName, - }, - null, - 2 - ) - ); - throw error; - } - }); - this.apiKey = apiKey; - } -} -exports.default = SupertokensService; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.d.ts deleted file mode 100644 index ef7c09e1d..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -import { ServiceInterface, TypeInput } from "../../../../../ingredients/smsdelivery/services/twilio"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -export default class TwilioService implements SmsDeliveryInterface { - serviceImpl: ServiceInterface; - private config; - constructor(config: TypeInput); - sendSms: ( - input: TypePasswordlessSmsDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.js b/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.js deleted file mode 100644 index 96a763dc0..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.js +++ /dev/null @@ -1,93 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const twilio_1 = require("../../../../../ingredients/smsdelivery/services/twilio"); -const twilio_2 = __importDefault(require("twilio")); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const serviceImplementation_1 = require("./serviceImplementation"); -class TwilioService { - constructor(config) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - let content = yield this.serviceImpl.getContent(input); - if ("from" in this.config.twilioSettings) { - yield this.serviceImpl.sendRawSms( - Object.assign(Object.assign({}, content), { - userContext: input.userContext, - from: this.config.twilioSettings.from, - }) - ); - } else { - yield this.serviceImpl.sendRawSms( - Object.assign(Object.assign({}, content), { - userContext: input.userContext, - messagingServiceSid: this.config.twilioSettings.messagingServiceSid, - }) - ); - } - }); - this.config = twilio_1.normaliseUserInputConfig(config); - const twilioClient = twilio_2.default( - config.twilioSettings.accountSid, - config.twilioSettings.authToken, - config.twilioSettings.opts - ); - let builder = new supertokens_js_override_1.default( - serviceImplementation_1.getServiceImplementation(twilioClient) - ); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } -} -exports.default = TwilioService; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.d.ts deleted file mode 100644 index 16af07d5e..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/smsdelivery/services/twilio"; -export default function getPasswordlessLoginSmsContent(input: TypePasswordlessSmsDeliveryInput): GetContentResult; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.js b/lib/build/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.js deleted file mode 100644 index 070ca9c75..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../../../utils"); -const supertokens_1 = __importDefault(require("../../../../../supertokens")); -function getPasswordlessLoginSmsContent(input) { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getPasswordlessLoginSmsBody(appName, input.codeLifetime, input.urlWithLinkCode, input.userInputCode); - return { - body, - toPhoneNumber: input.phoneNumber, - }; -} -exports.default = getPasswordlessLoginSmsContent; -function getPasswordlessLoginSmsBody(appName, codeLifetime, urlWithLinkCode, userInputCode) { - let message = ""; - if (urlWithLinkCode !== undefined && userInputCode !== undefined) { - message += `OTP to login is ${userInputCode} for ${appName}\n\nOR click ${urlWithLinkCode} to login.\n\n`; - } else if (urlWithLinkCode !== undefined) { - message += `Click ${urlWithLinkCode} to login to ${appName}\n\n`; - } else { - message += `OTP to login is ${userInputCode} for ${appName}\n\n`; - } - const humanisedCodeLifetime = utils_1.humaniseMilliseconds(codeLifetime); - message += `This is valid for ${humanisedCodeLifetime}.`; - return message; -} diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.d.ts deleted file mode 100644 index 6aa22d4d2..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import Twilio from "twilio/lib/rest/Twilio"; -import { ServiceInterface } from "../../../../../ingredients/smsdelivery/services/twilio"; -export declare function getServiceImplementation( - twilioClient: Twilio -): ServiceInterface; diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.js b/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.js deleted file mode 100644 index f41680526..000000000 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getServiceImplementation = void 0; -const passwordlessLogin_1 = __importDefault(require("./passwordlessLogin")); -function getServiceImplementation(twilioClient) { - return { - sendRawSms: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if ("from" in input) { - yield twilioClient.messages.create({ - to: input.toPhoneNumber, - body: input.body, - from: input.from, - }); - } else { - yield twilioClient.messages.create({ - to: input.toPhoneNumber, - body: input.body, - messagingServiceSid: input.messagingServiceSid, - }); - } - }); - }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return passwordlessLogin_1.default(input); - }); - }, - }; -} -exports.getServiceImplementation = getServiceImplementation; diff --git a/lib/build/recipe/passwordless/types.d.ts b/lib/build/recipe/passwordless/types.d.ts deleted file mode 100644 index d8fbe092d..000000000 --- a/lib/build/recipe/passwordless/types.d.ts +++ /dev/null @@ -1,364 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { - TypeInput as SmsDeliveryTypeInput, - TypeInputWithService as SmsDeliveryTypeInputWithService, -} from "../../ingredients/smsdelivery/types"; -import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; -export declare type User = { - id: string; - email?: string; - phoneNumber?: string; - timeJoined: number; -}; -export declare type TypeInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } -) & { - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - emailDelivery?: EmailDeliveryTypeInput; - smsDelivery?: SmsDeliveryTypeInput; - getCustomUserInputCode?: (userContext: any) => Promise | string; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber: (phoneNumber: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress: (email: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress: (email: string) => Promise | string | undefined; - validatePhoneNumber: (phoneNumber: string) => Promise | string | undefined; - } -) & { - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - getCustomUserInputCode?: (userContext: any) => Promise | string; - getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService; - getEmailDeliveryConfig: () => EmailDeliveryTypeInputWithService; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder: OverrideableBuilder) => APIInterface; - }; -}; -export declare type RecipeInterface = { - createCode: ( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { - userInputCode?: string; - userContext: any; - } - ) => Promise<{ - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - }>; - createNewCodeForDevice: (input: { - deviceId: string; - userInputCode?: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - } - | { - status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR"; - } - >; - consumeCode: ( - input: - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - userContext: any; - } - | { - linkCode: string; - preAuthSessionId: string; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | { - status: "RESTART_FLOW_ERROR"; - } - >; - getUserById: (input: { userId: string; userContext: any }) => Promise; - getUserByEmail: (input: { email: string; userContext: any }) => Promise; - getUserByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - updateUser: (input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; - revokeAllCodes: ( - input: - | { - email: string; - userContext: any; - } - | { - phoneNumber: string; - userContext: any; - } - ) => Promise<{ - status: "OK"; - }>; - revokeCode: (input: { - codeId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - }>; - listCodesByEmail: (input: { email: string; userContext: any }) => Promise; - listCodesByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - listCodesByDeviceId: (input: { deviceId: string; userContext: any }) => Promise; - listCodesByPreAuthSessionId: (input: { - preAuthSessionId: string; - userContext: any; - }) => Promise; -}; -export declare type DeviceType = { - preAuthSessionId: string; - failedCodeInputAttemptCount: number; - email?: string; - phoneNumber?: string; - codes: { - codeId: string; - timeCreated: string; - codeLifetime: number; - }[]; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - appInfo: NormalisedAppinfo; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - emailDelivery: EmailDeliveryIngredient; - smsDelivery: SmsDeliveryIngredient; -}; -export declare type APIInterface = { - createCodePOST?: ( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { - options: APIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - deviceId: string; - preAuthSessionId: string; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - } - | GeneralErrorResponse - >; - resendCodePOST?: ( - input: { - deviceId: string; - preAuthSessionId: string; - } & { - options: APIOptions; - userContext: any; - } - ) => Promise< - | GeneralErrorResponse - | { - status: "RESTART_FLOW_ERROR" | "OK"; - } - >; - consumeCodePOST?: ( - input: ( - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - } - | { - linkCode: string; - preAuthSessionId: string; - } - ) & { - options: APIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | GeneralErrorResponse - | { - status: "RESTART_FLOW_ERROR"; - } - >; - emailExistsGET?: (input: { - email: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >; - phoneNumberExistsGET?: (input: { - phoneNumber: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >; -}; -export declare type TypePasswordlessEmailDeliveryInput = { - type: "PASSWORDLESS_LOGIN"; - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; -}; -export declare type TypePasswordlessSmsDeliveryInput = { - type: "PASSWORDLESS_LOGIN"; - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; -}; diff --git a/lib/build/recipe/passwordless/types.js b/lib/build/recipe/passwordless/types.js deleted file mode 100644 index a098ca1d7..000000000 --- a/lib/build/recipe/passwordless/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/passwordless/utils.d.ts b/lib/build/recipe/passwordless/utils.d.ts deleted file mode 100644 index f00fc5184..000000000 --- a/lib/build/recipe/passwordless/utils.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput } from "./types"; -import { NormalisedAppinfo } from "../../types"; -export declare function validateAndNormaliseUserInput( - _: Recipe, - appInfo: NormalisedAppinfo, - config: TypeInput -): TypeNormalisedInput; -export declare function defaultValidatePhoneNumber(value: string): Promise | string | undefined; -export declare function defaultValidateEmail(value: string): Promise | string | undefined; diff --git a/lib/build/recipe/passwordless/utils.js b/lib/build/recipe/passwordless/utils.js deleted file mode 100644 index 2bbe5e2e2..000000000 --- a/lib/build/recipe/passwordless/utils.js +++ /dev/null @@ -1,171 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.defaultValidateEmail = exports.defaultValidatePhoneNumber = exports.validateAndNormaliseUserInput = void 0; -const max_1 = __importDefault(require("libphonenumber-js/max")); -const backwardCompatibility_1 = __importDefault(require("./emaildelivery/services/backwardCompatibility")); -const backwardCompatibility_2 = __importDefault(require("./smsdelivery/services/backwardCompatibility")); -function validateAndNormaliseUserInput(_, appInfo, config) { - if ( - config.contactMethod !== "PHONE" && - config.contactMethod !== "EMAIL" && - config.contactMethod !== "EMAIL_OR_PHONE" - ) { - throw new Error('Please pass one of "PHONE", "EMAIL" or "EMAIL_OR_PHONE" as the contactMethod'); - } - if (config.flowType === undefined) { - throw new Error("Please pass flowType argument in the config"); - } - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config.override - ); - function getEmailDeliveryConfig() { - var _a; - let emailService = (_a = config.emailDelivery) === null || _a === void 0 ? void 0 : _a.service; - let createAndSendCustomEmail = config.contactMethod === "PHONE" ? undefined : config.createAndSendCustomEmail; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation - */ - if (emailService === undefined) { - emailService = new backwardCompatibility_1.default(appInfo, createAndSendCustomEmail); - } - let emailDelivery = Object.assign(Object.assign({}, config.emailDelivery), { - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }); - return emailDelivery; - } - function getSmsDeliveryConfig() { - var _a; - let smsService = (_a = config.smsDelivery) === null || _a === void 0 ? void 0 : _a.service; - let createAndSendCustomTextMessage = - config.contactMethod === "EMAIL" ? undefined : config.createAndSendCustomTextMessage; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomTextMessage config. If the user - * has not passed even that config, we use the default - * createAndSendCustomTextMessage implementation - */ - if (smsService === undefined) { - smsService = new backwardCompatibility_2.default(appInfo, createAndSendCustomTextMessage); - } - let smsDelivery = Object.assign(Object.assign({}, config.smsDelivery), { - /** - * if we do - * let smsDelivery = { - * service: smsService, - * ...config.smsDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: smsService, - }); - return smsDelivery; - } - if (config.contactMethod === "EMAIL") { - return { - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - flowType: config.flowType, - contactMethod: "EMAIL", - validateEmailAddress: - config.validateEmailAddress === undefined ? defaultValidateEmail : config.validateEmailAddress, - getCustomUserInputCode: config.getCustomUserInputCode, - }; - } else if (config.contactMethod === "PHONE") { - return { - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - flowType: config.flowType, - contactMethod: "PHONE", - validatePhoneNumber: - config.validatePhoneNumber === undefined ? defaultValidatePhoneNumber : config.validatePhoneNumber, - getCustomUserInputCode: config.getCustomUserInputCode, - }; - } else { - return { - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - flowType: config.flowType, - contactMethod: "EMAIL_OR_PHONE", - validateEmailAddress: - config.validateEmailAddress === undefined ? defaultValidateEmail : config.validateEmailAddress, - validatePhoneNumber: - config.validatePhoneNumber === undefined ? defaultValidatePhoneNumber : config.validatePhoneNumber, - getCustomUserInputCode: config.getCustomUserInputCode, - }; - } -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; -function defaultValidatePhoneNumber(value) { - if (typeof value !== "string") { - return "Development bug: Please make sure the phoneNumber field is a string"; - } - let parsedPhoneNumber = max_1.default(value); - if (parsedPhoneNumber === undefined || !parsedPhoneNumber.isValid()) { - return "Phone number is invalid"; - } - // we can even use Twilio's phone number validity lookup service: https://www.twilio.com/docs/glossary/what-e164. - return undefined; -} -exports.defaultValidatePhoneNumber = defaultValidatePhoneNumber; -function defaultValidateEmail(value) { - // We check if the email syntax is correct - // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - // Regex from https://stackoverflow.com/a/46181/3867175 - if (typeof value !== "string") { - return "Development bug: Please make sure the email field is a string"; - } - if ( - value.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) === null - ) { - return "Email is invalid"; - } - return undefined; -} -exports.defaultValidateEmail = defaultValidateEmail; diff --git a/lib/build/recipe/session/accessToken.d.ts b/lib/build/recipe/session/accessToken.d.ts deleted file mode 100644 index 2ba473703..000000000 --- a/lib/build/recipe/session/accessToken.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -import { ParsedJWTInfo } from "./jwt"; -export declare function getInfoFromAccessToken( - jwtInfo: ParsedJWTInfo, - jwtSigningPublicKey: string, - doAntiCsrfCheck: boolean -): Promise<{ - sessionHandle: string; - userId: string; - refreshTokenHash1: string; - parentRefreshTokenHash1: string | undefined; - userData: any; - antiCsrfToken: string | undefined; - expiryTime: number; - timeCreated: number; -}>; -export declare function validateAccessTokenStructure(payload: any): void; -export declare function sanitizeNumberInput(field: any): number | undefined; diff --git a/lib/build/recipe/session/accessToken.js b/lib/build/recipe/session/accessToken.js deleted file mode 100644 index 84d62cc9b..000000000 --- a/lib/build/recipe/session/accessToken.js +++ /dev/null @@ -1,130 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sanitizeNumberInput = exports.validateAccessTokenStructure = exports.getInfoFromAccessToken = void 0; -const error_1 = __importDefault(require("./error")); -const jwt_1 = require("./jwt"); -function getInfoFromAccessToken(jwtInfo, jwtSigningPublicKey, doAntiCsrfCheck) { - return __awaiter(this, void 0, void 0, function* () { - try { - jwt_1.verifyJWT(jwtInfo, jwtSigningPublicKey); - const payload = jwtInfo.payload; - // This should be called before this function, but the check is very quick, so we can also do them here - validateAccessTokenStructure(payload); - // We can mark these as defined (the ! after the calls), since validateAccessTokenPayload checks this - let sessionHandle = sanitizeStringInput(payload.sessionHandle); - let userId = sanitizeStringInput(payload.userId); - let refreshTokenHash1 = sanitizeStringInput(payload.refreshTokenHash1); - let parentRefreshTokenHash1 = sanitizeStringInput(payload.parentRefreshTokenHash1); - let userData = payload.userData; - let antiCsrfToken = sanitizeStringInput(payload.antiCsrfToken); - let expiryTime = sanitizeNumberInput(payload.expiryTime); - let timeCreated = sanitizeNumberInput(payload.timeCreated); - if (antiCsrfToken === undefined && doAntiCsrfCheck) { - throw Error("Access token does not contain the anti-csrf token."); - } - if (expiryTime < Date.now()) { - throw Error("Access token expired"); - } - return { - sessionHandle, - userId, - refreshTokenHash1, - parentRefreshTokenHash1, - userData, - antiCsrfToken, - expiryTime, - timeCreated, - }; - } catch (err) { - throw new error_1.default({ - message: "Failed to verify access token", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - }); -} -exports.getInfoFromAccessToken = getInfoFromAccessToken; -function validateAccessTokenStructure(payload) { - if ( - typeof payload.sessionHandle !== "string" || - typeof payload.userId !== "string" || - typeof payload.refreshTokenHash1 !== "string" || - payload.userData === undefined || - typeof payload.expiryTime !== "number" || - typeof payload.timeCreated !== "number" - ) { - // it would come here if we change the structure of the JWT. - throw Error("Access token does not contain all the information. Maybe the structure has changed?"); - } -} -exports.validateAccessTokenStructure = validateAccessTokenStructure; -function sanitizeStringInput(field) { - if (field === "") { - return ""; - } - if (typeof field !== "string") { - return undefined; - } - try { - let result = field.trim(); - return result; - } catch (err) {} - return undefined; -} -function sanitizeNumberInput(field) { - if (typeof field === "number") { - return field; - } - return undefined; -} -exports.sanitizeNumberInput = sanitizeNumberInput; diff --git a/lib/build/recipe/session/api/implementation.d.ts b/lib/build/recipe/session/api/implementation.d.ts deleted file mode 100644 index dd40e7025..000000000 --- a/lib/build/recipe/session/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../"; -export default function getAPIInterface(): APIInterface; diff --git a/lib/build/recipe/session/api/implementation.js b/lib/build/recipe/session/api/implementation.js deleted file mode 100644 index 517833f85..000000000 --- a/lib/build/recipe/session/api/implementation.js +++ /dev/null @@ -1,100 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -const utils_2 = require("../utils"); -function getAPIInterface() { - return { - refreshPOST: function ({ options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - return yield options.recipeImplementation.refreshSession({ - req: options.req, - res: options.res, - userContext, - }); - }); - }, - verifySession: function ({ verifySessionOptions, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let method = utils_1.normaliseHttpMethod(options.req.getMethod()); - if (method === "options" || method === "trace") { - return undefined; - } - let incomingPath = new normalisedURLPath_1.default(options.req.getOriginalURL()); - let refreshTokenPath = options.config.refreshTokenPath; - if (incomingPath.equals(refreshTokenPath) && method === "post") { - return options.recipeImplementation.refreshSession({ - req: options.req, - res: options.res, - userContext, - }); - } else { - const session = yield options.recipeImplementation.getSession({ - req: options.req, - res: options.res, - options: verifySessionOptions, - userContext, - }); - if (session !== undefined) { - const claimValidators = yield utils_2.getRequiredClaimValidators( - session, - verifySessionOptions === null || verifySessionOptions === void 0 - ? void 0 - : verifySessionOptions.overrideGlobalClaimValidators, - userContext - ); - yield session.assertClaims(claimValidators, userContext); - } - return session; - } - }); - }, - signOutPOST: function ({ session, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - if (session !== undefined) { - yield session.revokeSession(userContext); - } - return { - status: "OK", - }; - }); - }, - }; -} -exports.default = getAPIInterface; diff --git a/lib/build/recipe/session/api/refresh.d.ts b/lib/build/recipe/session/api/refresh.d.ts deleted file mode 100644 index 6b4746e67..000000000 --- a/lib/build/recipe/session/api/refresh.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function handleRefreshAPI(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/session/api/refresh.js b/lib/build/recipe/session/api/refresh.js deleted file mode 100644 index 6ae4b4a31..000000000 --- a/lib/build/recipe/session/api/refresh.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("../../../utils"); -function handleRefreshAPI(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.refreshPOST === undefined) { - return false; - } - yield apiImplementation.refreshPOST({ - options, - userContext: utils_2.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response(options.res, {}); - return true; - }); -} -exports.default = handleRefreshAPI; diff --git a/lib/build/recipe/session/api/signout.d.ts b/lib/build/recipe/session/api/signout.d.ts deleted file mode 100644 index 0e1d985d3..000000000 --- a/lib/build/recipe/session/api/signout.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function signOutAPI(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/session/api/signout.js b/lib/build/recipe/session/api/signout.js deleted file mode 100644 index d0ca3e50c..000000000 --- a/lib/build/recipe/session/api/signout.js +++ /dev/null @@ -1,75 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const utils_2 = require("../../../utils"); -function signOutAPI(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - // Logic as per https://github.com/supertokens/supertokens-node/issues/34#issuecomment-717958537 - if (apiImplementation.signOutPOST === undefined) { - return false; - } - let defaultUserContext = utils_2.makeDefaultUserContextFromAPI(options.req); - const session = yield options.recipeImplementation.getSession({ - req: options.req, - res: options.res, - options: { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, - userContext: defaultUserContext, - }); - let result = yield apiImplementation.signOutPOST({ - options, - session, - userContext: defaultUserContext, - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = signOutAPI; diff --git a/lib/build/recipe/session/claimBaseClasses/booleanClaim.d.ts b/lib/build/recipe/session/claimBaseClasses/booleanClaim.d.ts deleted file mode 100644 index ba5736f3f..000000000 --- a/lib/build/recipe/session/claimBaseClasses/booleanClaim.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -import { SessionClaim, SessionClaimValidator } from "../types"; -import { PrimitiveClaim } from "./primitiveClaim"; -export declare class BooleanClaim extends PrimitiveClaim { - constructor(conf: { - key: string; - fetchValue: SessionClaim["fetchValue"]; - defaultMaxAgeInSeconds?: number; - }); - validators: PrimitiveClaim["validators"] & { - isTrue: (maxAge?: number, id?: string) => SessionClaimValidator; - isFalse: (maxAge?: number, id?: string) => SessionClaimValidator; - }; -} diff --git a/lib/build/recipe/session/claimBaseClasses/booleanClaim.js b/lib/build/recipe/session/claimBaseClasses/booleanClaim.js deleted file mode 100644 index 37e233d3f..000000000 --- a/lib/build/recipe/session/claimBaseClasses/booleanClaim.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BooleanClaim = void 0; -const primitiveClaim_1 = require("./primitiveClaim"); -class BooleanClaim extends primitiveClaim_1.PrimitiveClaim { - constructor(conf) { - super(conf); - this.validators = Object.assign(Object.assign({}, this.validators), { - isTrue: (maxAge, id) => this.validators.hasValue(true, maxAge, id), - isFalse: (maxAge, id) => this.validators.hasValue(false, maxAge, id), - }); - } -} -exports.BooleanClaim = BooleanClaim; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts deleted file mode 100644 index 8f765d22a..000000000 --- a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -import { JSONPrimitive } from "../../../types"; -import { SessionClaim, SessionClaimValidator } from "../types"; -export declare class PrimitiveArrayClaim extends SessionClaim { - readonly fetchValue: (userId: string, userContext: any) => Promise | T[] | undefined; - readonly defaultMaxAgeInSeconds: number | undefined; - constructor(config: { key: string; fetchValue: SessionClaim["fetchValue"]; defaultMaxAgeInSeconds?: number }); - addToPayload_internal(payload: any, value: T[], _userContext: any): any; - removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any; - removeFromPayload(payload: any, _userContext?: any): any; - getValueFromPayload(payload: any, _userContext?: any): T[] | undefined; - getLastRefetchTime(payload: any, _userContext?: any): number | undefined; - validators: { - includes: (val: T, maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; - excludes: (val: T, maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; - includesAll: (val: T[], maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; - excludesAll: (val: T[], maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; - }; -} diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.js b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.js deleted file mode 100644 index 1b95308b2..000000000 --- a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.js +++ /dev/null @@ -1,248 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PrimitiveArrayClaim = void 0; -const types_1 = require("../types"); -class PrimitiveArrayClaim extends types_1.SessionClaim { - constructor(config) { - super(config.key); - this.validators = { - includes: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { - return { - claim: this, - id: id !== null && id !== void 0 ? id : this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (!claimVal.includes(val)) { - return { - isValid: false, - reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, - }; - } - return { isValid: true }; - }), - }; - }, - excludes: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { - return { - claim: this, - id: id !== null && id !== void 0 ? id : this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (claimVal.includes(val)) { - return { - isValid: false, - reason: { - message: "wrong value", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - return { isValid: true }; - }), - }; - }, - includesAll: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { - return { - claim: this, - id: id !== null && id !== void 0 ? id : this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - const claimSet = new Set(claimVal); - const isValid = val.every((v) => claimSet.has(v)); - return isValid - ? { isValid } - : { - isValid, - reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, - }; - }), - }; - }, - excludesAll: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { - return { - claim: this, - id: id !== null && id !== void 0 ? id : this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - const claimSet = new Set(claimVal); - const isValid = val.every((v) => !claimSet.has(v)); - return isValid - ? { isValid: isValid } - : { - isValid, - reason: { - message: "wrong value", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - }), - }; - }, - }; - this.fetchValue = config.fetchValue; - this.defaultMaxAgeInSeconds = config.defaultMaxAgeInSeconds; - } - addToPayload_internal(payload, value, _userContext) { - return Object.assign(Object.assign({}, payload), { - [this.key]: { - v: value, - t: Date.now(), - }, - }); - } - removeFromPayloadByMerge_internal(payload, _userContext) { - const res = Object.assign(Object.assign({}, payload), { [this.key]: null }); - return res; - } - removeFromPayload(payload, _userContext) { - const res = Object.assign({}, payload); - delete res[this.key]; - return res; - } - getValueFromPayload(payload, _userContext) { - var _a; - return (_a = payload[this.key]) === null || _a === void 0 ? void 0 : _a.v; - } - getLastRefetchTime(payload, _userContext) { - var _a; - return (_a = payload[this.key]) === null || _a === void 0 ? void 0 : _a.t; - } -} -exports.PrimitiveArrayClaim = PrimitiveArrayClaim; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts deleted file mode 100644 index 48e8cbff1..000000000 --- a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -import { JSONPrimitive } from "../../../types"; -import { SessionClaim, SessionClaimValidator } from "../types"; -export declare class PrimitiveClaim extends SessionClaim { - readonly fetchValue: (userId: string, userContext: any) => Promise | T | undefined; - readonly defaultMaxAgeInSeconds: number | undefined; - constructor(config: { key: string; fetchValue: SessionClaim["fetchValue"]; defaultMaxAgeInSeconds?: number }); - addToPayload_internal(payload: any, value: T, _userContext: any): any; - removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any; - removeFromPayload(payload: any, _userContext?: any): any; - getValueFromPayload(payload: any, _userContext?: any): T | undefined; - getLastRefetchTime(payload: any, _userContext?: any): number | undefined; - validators: { - hasValue: (val: T, maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; - }; -} diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.js b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.js deleted file mode 100644 index ff72f2b4e..000000000 --- a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PrimitiveClaim = void 0; -const types_1 = require("../types"); -class PrimitiveClaim extends types_1.SessionClaim { - constructor(config) { - super(config.key); - this.validators = { - hasValue: (val, maxAgeInSeconds = this.defaultMaxAgeInSeconds, id) => { - return { - claim: this, - id: id !== null && id !== void 0 ? id : this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - (maxAgeInSeconds !== undefined && // We know payload[this.id] is defined since the value is not undefined in this branch - payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: (payload, ctx) => - __awaiter(this, void 0, void 0, function* () { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedValue: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (claimVal !== val) { - return { - isValid: false, - reason: { message: "wrong value", expectedValue: val, actualValue: claimVal }, - }; - } - return { isValid: true }; - }), - }; - }, - }; - this.fetchValue = config.fetchValue; - this.defaultMaxAgeInSeconds = config.defaultMaxAgeInSeconds; - } - addToPayload_internal(payload, value, _userContext) { - return Object.assign(Object.assign({}, payload), { - [this.key]: { - v: value, - t: Date.now(), - }, - }); - } - removeFromPayloadByMerge_internal(payload, _userContext) { - const res = Object.assign(Object.assign({}, payload), { [this.key]: null }); - return res; - } - removeFromPayload(payload, _userContext) { - const res = Object.assign({}, payload); - delete res[this.key]; - return res; - } - getValueFromPayload(payload, _userContext) { - var _a; - return (_a = payload[this.key]) === null || _a === void 0 ? void 0 : _a.v; - } - getLastRefetchTime(payload, _userContext) { - var _a; - return (_a = payload[this.key]) === null || _a === void 0 ? void 0 : _a.t; - } -} -exports.PrimitiveClaim = PrimitiveClaim; diff --git a/lib/build/recipe/session/claims.d.ts b/lib/build/recipe/session/claims.d.ts deleted file mode 100644 index ae6b132bd..000000000 --- a/lib/build/recipe/session/claims.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -export { SessionClaim } from "./types"; -export { PrimitiveClaim } from "./claimBaseClasses/primitiveClaim"; -export { PrimitiveArrayClaim } from "./claimBaseClasses/primitiveArrayClaim"; -export { BooleanClaim } from "./claimBaseClasses/booleanClaim"; diff --git a/lib/build/recipe/session/claims.js b/lib/build/recipe/session/claims.js deleted file mode 100644 index 30fae1663..000000000 --- a/lib/build/recipe/session/claims.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BooleanClaim = exports.PrimitiveArrayClaim = exports.PrimitiveClaim = exports.SessionClaim = void 0; -var types_1 = require("./types"); -Object.defineProperty(exports, "SessionClaim", { - enumerable: true, - get: function () { - return types_1.SessionClaim; - }, -}); -var primitiveClaim_1 = require("./claimBaseClasses/primitiveClaim"); -Object.defineProperty(exports, "PrimitiveClaim", { - enumerable: true, - get: function () { - return primitiveClaim_1.PrimitiveClaim; - }, -}); -var primitiveArrayClaim_1 = require("./claimBaseClasses/primitiveArrayClaim"); -Object.defineProperty(exports, "PrimitiveArrayClaim", { - enumerable: true, - get: function () { - return primitiveArrayClaim_1.PrimitiveArrayClaim; - }, -}); -var booleanClaim_1 = require("./claimBaseClasses/booleanClaim"); -Object.defineProperty(exports, "BooleanClaim", { - enumerable: true, - get: function () { - return booleanClaim_1.BooleanClaim; - }, -}); diff --git a/lib/build/recipe/session/constants.d.ts b/lib/build/recipe/session/constants.d.ts deleted file mode 100644 index 4f1b5bd68..000000000 --- a/lib/build/recipe/session/constants.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import { TokenTransferMethod } from "./types"; -export declare const REFRESH_API_PATH = "/session/refresh"; -export declare const SIGNOUT_API_PATH = "/signout"; -export declare const availableTokenTransferMethods: TokenTransferMethod[]; diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js deleted file mode 100644 index 742008e66..000000000 --- a/lib/build/recipe/session/constants.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.availableTokenTransferMethods = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; -exports.REFRESH_API_PATH = "/session/refresh"; -exports.SIGNOUT_API_PATH = "/signout"; -exports.availableTokenTransferMethods = ["cookie", "header"]; diff --git a/lib/build/recipe/session/cookieAndHeaders.d.ts b/lib/build/recipe/session/cookieAndHeaders.d.ts deleted file mode 100644 index 40cd2f2bf..000000000 --- a/lib/build/recipe/session/cookieAndHeaders.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import { TokenTransferMethod, TokenType, TypeNormalisedInput } from "./types"; -export declare function clearSessionFromAllTokenTransferMethods(config: TypeNormalisedInput, res: BaseResponse): void; -export declare function clearSession( - config: TypeNormalisedInput, - res: BaseResponse, - transferMethod: TokenTransferMethod -): void; -export declare function getAntiCsrfTokenFromHeaders(req: BaseRequest): string | undefined; -export declare function setAntiCsrfTokenInHeaders(res: BaseResponse, antiCsrfToken: string): void; -export declare function setFrontTokenInHeaders( - res: BaseResponse, - userId: string, - atExpiry: number, - accessTokenPayload: any -): void; -export declare function getCORSAllowedHeaders(): string[]; -export declare function getToken( - req: BaseRequest, - tokenType: TokenType, - transferMethod: TokenTransferMethod -): string | undefined; -export declare function setToken( - config: TypeNormalisedInput, - res: BaseResponse, - tokenType: TokenType, - value: string, - expires: number, - transferMethod: TokenTransferMethod -): void; -export declare function setHeader(res: BaseResponse, name: string, value: string): void; -/** - * - * @param res - * @param name - * @param value - * @param domain - * @param secure - * @param httpOnly - * @param expires - * @param path - */ -export declare function setCookie( - config: TypeNormalisedInput, - res: BaseResponse, - name: string, - value: string, - expires: number, - pathType: "refreshTokenPath" | "accessTokenPath" -): void; -export declare function getAuthModeFromHeader(req: BaseRequest): string | undefined; diff --git a/lib/build/recipe/session/cookieAndHeaders.js b/lib/build/recipe/session/cookieAndHeaders.js deleted file mode 100644 index 3133c5698..000000000 --- a/lib/build/recipe/session/cookieAndHeaders.js +++ /dev/null @@ -1,159 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getAuthModeFromHeader = exports.setCookie = exports.setHeader = exports.setToken = exports.getToken = exports.getCORSAllowedHeaders = exports.setFrontTokenInHeaders = exports.setAntiCsrfTokenInHeaders = exports.getAntiCsrfTokenFromHeaders = exports.clearSession = exports.clearSessionFromAllTokenTransferMethods = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const constants_1 = require("../../constants"); -const constants_2 = require("./constants"); -const authorizationHeaderKey = "authorization"; -const accessTokenCookieKey = "sAccessToken"; -const accessTokenHeaderKey = "st-access-token"; -const refreshTokenCookieKey = "sRefreshToken"; -const refreshTokenHeaderKey = "st-refresh-token"; -const antiCsrfHeaderKey = "anti-csrf"; -const frontTokenHeaderKey = "front-token"; -const authModeHeaderKey = "st-auth-mode"; -function clearSessionFromAllTokenTransferMethods(config, res) { - // We are clearing the session in all transfermethods to be sure to override cookies in case they have been already added to the response. - // This is done to handle the following use-case: - // If the app overrides signInPOST to check the ban status of the user after the original implementation and throwing an UNAUTHORISED error - // In this case: the SDK has attached cookies to the response, but none was sent with the request - // We can't know which to clear since we can't reliably query or remove the set-cookie header added to the response (causes issues in some frameworks, i.e.: hapi) - // The safe solution in this case is to overwrite all the response cookies/headers with an empty value, which is what we are doing here - for (const transferMethod of constants_2.availableTokenTransferMethods) { - clearSession(config, res, transferMethod); - } -} -exports.clearSessionFromAllTokenTransferMethods = clearSessionFromAllTokenTransferMethods; -function clearSession(config, res, transferMethod) { - // If we can be specific about which transferMethod we want to clear, there is no reason to clear the other ones - const tokenTypes = ["access", "refresh"]; - for (const token of tokenTypes) { - setToken(config, res, token, "", 0, transferMethod); - } - res.removeHeader(antiCsrfHeaderKey); - // This can be added multiple times in some cases, but that should be OK - res.setHeader(frontTokenHeaderKey, "remove", false); - res.setHeader("Access-Control-Expose-Headers", frontTokenHeaderKey, true); -} -exports.clearSession = clearSession; -function getAntiCsrfTokenFromHeaders(req) { - return req.getHeaderValue(antiCsrfHeaderKey); -} -exports.getAntiCsrfTokenFromHeaders = getAntiCsrfTokenFromHeaders; -function setAntiCsrfTokenInHeaders(res, antiCsrfToken) { - res.setHeader(antiCsrfHeaderKey, antiCsrfToken, false); - res.setHeader("Access-Control-Expose-Headers", antiCsrfHeaderKey, true); -} -exports.setAntiCsrfTokenInHeaders = setAntiCsrfTokenInHeaders; -function setFrontTokenInHeaders(res, userId, atExpiry, accessTokenPayload) { - const tokenInfo = { - uid: userId, - ate: atExpiry, - up: accessTokenPayload, - }; - res.setHeader(frontTokenHeaderKey, Buffer.from(JSON.stringify(tokenInfo)).toString("base64"), false); - res.setHeader("Access-Control-Expose-Headers", frontTokenHeaderKey, true); -} -exports.setFrontTokenInHeaders = setFrontTokenInHeaders; -function getCORSAllowedHeaders() { - return [antiCsrfHeaderKey, constants_1.HEADER_RID, authorizationHeaderKey, authModeHeaderKey]; -} -exports.getCORSAllowedHeaders = getCORSAllowedHeaders; -function getCookieNameFromTokenType(tokenType) { - switch (tokenType) { - case "access": - return accessTokenCookieKey; - case "refresh": - return refreshTokenCookieKey; - default: - throw new Error("Unknown token type, should never happen."); - } -} -function getResponseHeaderNameForTokenType(tokenType) { - switch (tokenType) { - case "access": - return accessTokenHeaderKey; - case "refresh": - return refreshTokenHeaderKey; - default: - throw new Error("Unknown token type, should never happen."); - } -} -function getToken(req, tokenType, transferMethod) { - if (transferMethod === "cookie") { - return req.getCookieValue(getCookieNameFromTokenType(tokenType)); - } else if (transferMethod === "header") { - const value = req.getHeaderValue(authorizationHeaderKey); - if (value === undefined || !value.startsWith("Bearer ")) { - return undefined; - } - return value.replace(/^Bearer /, "").trim(); - } else { - throw new Error("Should never happen: Unknown transferMethod: " + transferMethod); - } -} -exports.getToken = getToken; -function setToken(config, res, tokenType, value, expires, transferMethod) { - if (transferMethod === "cookie") { - setCookie( - config, - res, - getCookieNameFromTokenType(tokenType), - value, - expires, - tokenType === "refresh" ? "refreshTokenPath" : "accessTokenPath" - ); - } else if (transferMethod === "header") { - setHeader(res, getResponseHeaderNameForTokenType(tokenType), value); - } -} -exports.setToken = setToken; -function setHeader(res, name, value) { - res.setHeader(name, value, false); - res.setHeader("Access-Control-Expose-Headers", name, true); -} -exports.setHeader = setHeader; -/** - * - * @param res - * @param name - * @param value - * @param domain - * @param secure - * @param httpOnly - * @param expires - * @param path - */ -function setCookie(config, res, name, value, expires, pathType) { - let domain = config.cookieDomain; - let secure = config.cookieSecure; - let sameSite = config.cookieSameSite; - let path = ""; - if (pathType === "refreshTokenPath") { - path = config.refreshTokenPath.getAsStringDangerous(); - } else if (pathType === "accessTokenPath") { - path = - config.accessTokenPath.getAsStringDangerous() === "" ? "/" : config.accessTokenPath.getAsStringDangerous(); - } - let httpOnly = true; - return res.setCookie(name, value, domain, secure, httpOnly, expires, path, sameSite); -} -exports.setCookie = setCookie; -function getAuthModeFromHeader(req) { - var _a; - return (_a = req.getHeaderValue(authModeHeaderKey)) === null || _a === void 0 ? void 0 : _a.toLowerCase(); -} -exports.getAuthModeFromHeader = getAuthModeFromHeader; diff --git a/lib/build/recipe/session/error.d.ts b/lib/build/recipe/session/error.d.ts deleted file mode 100644 index 9ff25e067..000000000 --- a/lib/build/recipe/session/error.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -import { ClaimValidationError } from "./types"; -export default class SessionError extends STError { - static UNAUTHORISED: "UNAUTHORISED"; - static TRY_REFRESH_TOKEN: "TRY_REFRESH_TOKEN"; - static TOKEN_THEFT_DETECTED: "TOKEN_THEFT_DETECTED"; - static INVALID_CLAIMS: "INVALID_CLAIMS"; - constructor( - options: - | { - message: string; - type: "UNAUTHORISED"; - payload?: { - clearTokens: boolean; - }; - } - | { - message: string; - type: "TRY_REFRESH_TOKEN"; - } - | { - message: string; - type: "TOKEN_THEFT_DETECTED"; - payload: { - userId: string; - sessionHandle: string; - }; - } - | { - message: string; - type: "INVALID_CLAIMS"; - payload: ClaimValidationError[]; - } - ); -} diff --git a/lib/build/recipe/session/error.js b/lib/build/recipe/session/error.js deleted file mode 100644 index 24c92ef59..000000000 --- a/lib/build/recipe/session/error.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class SessionError extends error_1.default { - constructor(options) { - super( - options.type === "UNAUTHORISED" && options.payload === undefined - ? Object.assign(Object.assign({}, options), { - payload: { - clearTokens: true, - }, - }) - : Object.assign({}, options) - ); - this.fromRecipe = "session"; - } -} -exports.default = SessionError; -SessionError.UNAUTHORISED = "UNAUTHORISED"; -SessionError.TRY_REFRESH_TOKEN = "TRY_REFRESH_TOKEN"; -SessionError.TOKEN_THEFT_DETECTED = "TOKEN_THEFT_DETECTED"; -SessionError.INVALID_CLAIMS = "INVALID_CLAIMS"; diff --git a/lib/build/recipe/session/framework/awsLambda.d.ts b/lib/build/recipe/session/framework/awsLambda.d.ts deleted file mode 100644 index 2b5f88975..000000000 --- a/lib/build/recipe/session/framework/awsLambda.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import type { Handler } from "aws-lambda"; -import { VerifySessionOptions } from ".."; -export declare function verifySession(handler: Handler, verifySessionOptions?: VerifySessionOptions): Handler; diff --git a/lib/build/recipe/session/framework/awsLambda.js b/lib/build/recipe/session/framework/awsLambda.js deleted file mode 100644 index ea8086157..000000000 --- a/lib/build/recipe/session/framework/awsLambda.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifySession = void 0; -const framework_1 = require("../../../framework/awsLambda/framework"); -const supertokens_1 = __importDefault(require("../../../supertokens")); -const recipe_1 = __importDefault(require("../recipe")); -function verifySession(handler, verifySessionOptions) { - return (event, context, callback) => - __awaiter(this, void 0, void 0, function* () { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - let request = new framework_1.AWSRequest(event); - let response = new framework_1.AWSResponse(event); - try { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - event.session = yield sessionRecipe.verifySession(verifySessionOptions, request, response); - let handlerResult = yield handler(event, context, callback); - return response.sendResponse(handlerResult); - } catch (err) { - yield supertokens.errorHandler(err, request, response); - if (response.responseSet) { - return response.sendResponse({}); - } - throw err; - } - }); -} -exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/express.d.ts b/lib/build/recipe/session/framework/express.d.ts deleted file mode 100644 index bcb0bf234..000000000 --- a/lib/build/recipe/session/framework/express.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import type { VerifySessionOptions } from ".."; -import type { SessionRequest } from "../../../framework/express/framework"; -import type { NextFunction, Response } from "express"; -export declare function verifySession( - options?: VerifySessionOptions -): (req: SessionRequest, res: Response, next: NextFunction) => Promise; diff --git a/lib/build/recipe/session/framework/express.js b/lib/build/recipe/session/framework/express.js deleted file mode 100644 index 94346e430..000000000 --- a/lib/build/recipe/session/framework/express.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifySession = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipe_1 = __importDefault(require("../recipe")); -const framework_1 = require("../../../framework/express/framework"); -const supertokens_1 = __importDefault(require("../../../supertokens")); -function verifySession(options) { - return (req, res, next) => - __awaiter(this, void 0, void 0, function* () { - const request = new framework_1.ExpressRequest(req); - const response = new framework_1.ExpressResponse(res); - try { - const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - req.session = yield sessionRecipe.verifySession(options, request, response); - next(); - } catch (err) { - try { - const supertokens = supertokens_1.default.getInstanceOrThrowError(); - yield supertokens.errorHandler(err, request, response); - } catch (_a) { - next(err); - } - } - }); -} -exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/fastify.d.ts b/lib/build/recipe/session/framework/fastify.d.ts deleted file mode 100644 index a097f307a..000000000 --- a/lib/build/recipe/session/framework/fastify.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { VerifySessionOptions } from ".."; -import { SessionRequest } from "../../../framework/fastify/framework"; -import { FastifyReply } from "fastify"; -export declare function verifySession( - options?: VerifySessionOptions -): (req: SessionRequest, res: FastifyReply) => Promise; diff --git a/lib/build/recipe/session/framework/fastify.js b/lib/build/recipe/session/framework/fastify.js deleted file mode 100644 index 2a361bb97..000000000 --- a/lib/build/recipe/session/framework/fastify.js +++ /dev/null @@ -1,72 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifySession = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipe_1 = __importDefault(require("../recipe")); -const framework_1 = require("../../../framework/fastify/framework"); -const supertokens_1 = __importDefault(require("../../../supertokens")); -function verifySession(options) { - return (req, res) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let request = new framework_1.FastifyRequest(req); - let response = new framework_1.FastifyResponse(res); - try { - req.session = yield sessionRecipe.verifySession(options, request, response); - } catch (err) { - const supertokens = supertokens_1.default.getInstanceOrThrowError(); - yield supertokens.errorHandler(err, request, response); - throw err; - } - }); -} -exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/hapi.d.ts b/lib/build/recipe/session/framework/hapi.d.ts deleted file mode 100644 index 0181a9012..000000000 --- a/lib/build/recipe/session/framework/hapi.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { VerifySessionOptions } from ".."; -import { ResponseToolkit } from "@hapi/hapi"; -import { SessionRequest } from "../../../framework/hapi/framework"; -export declare function verifySession( - options?: VerifySessionOptions -): (req: SessionRequest, h: ResponseToolkit) => Promise; diff --git a/lib/build/recipe/session/framework/hapi.js b/lib/build/recipe/session/framework/hapi.js deleted file mode 100644 index 369f6069a..000000000 --- a/lib/build/recipe/session/framework/hapi.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifySession = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipe_1 = __importDefault(require("../recipe")); -const framework_1 = require("../../../framework/hapi/framework"); -function verifySession(options) { - return (req, h) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let request = new framework_1.HapiRequest(req); - let response = new framework_1.HapiResponse(h); - req.session = yield sessionRecipe.verifySession(options, request, response); - return h.continue; - }); -} -exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/index.d.ts b/lib/build/recipe/session/framework/index.d.ts deleted file mode 100644 index dfba9d1f7..000000000 --- a/lib/build/recipe/session/framework/index.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -import * as expressFramework from "./express"; -import * as fastifyFramework from "./fastify"; -import * as hapiFramework from "./hapi"; -import * as loopbackFramework from "./loopback"; -import * as koaFramework from "./koa"; -import * as awsLambdaFramework from "./awsLambda"; -declare const _default: { - express: typeof expressFramework; - fastify: typeof fastifyFramework; - hapi: typeof hapiFramework; - loopback: typeof loopbackFramework; - koa: typeof koaFramework; - awsLambda: typeof awsLambdaFramework; -}; -export default _default; -export declare let express: typeof expressFramework; -export declare let fastify: typeof fastifyFramework; -export declare let hapi: typeof hapiFramework; -export declare let loopback: typeof loopbackFramework; -export declare let koa: typeof koaFramework; -export declare let awsLambda: typeof awsLambdaFramework; diff --git a/lib/build/recipe/session/framework/index.js b/lib/build/recipe/session/framework/index.js deleted file mode 100644 index 8aa3b653e..000000000 --- a/lib/build/recipe/session/framework/index.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.awsLambda = exports.koa = exports.loopback = exports.hapi = exports.fastify = exports.express = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const expressFramework = __importStar(require("./express")); -const fastifyFramework = __importStar(require("./fastify")); -const hapiFramework = __importStar(require("./hapi")); -const loopbackFramework = __importStar(require("./loopback")); -const koaFramework = __importStar(require("./koa")); -const awsLambdaFramework = __importStar(require("./awsLambda")); -exports.default = { - express: expressFramework, - fastify: fastifyFramework, - hapi: hapiFramework, - loopback: loopbackFramework, - koa: koaFramework, - awsLambda: awsLambdaFramework, -}; -exports.express = expressFramework; -exports.fastify = fastifyFramework; -exports.hapi = hapiFramework; -exports.loopback = loopbackFramework; -exports.koa = koaFramework; -exports.awsLambda = awsLambdaFramework; diff --git a/lib/build/recipe/session/framework/koa.d.ts b/lib/build/recipe/session/framework/koa.d.ts deleted file mode 100644 index 9e23ae80b..000000000 --- a/lib/build/recipe/session/framework/koa.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import type { VerifySessionOptions } from ".."; -import type { Next } from "koa"; -import type { SessionContext } from "../../../framework/koa/framework"; -export declare function verifySession( - options?: VerifySessionOptions -): (ctx: SessionContext, next: Next) => Promise; diff --git a/lib/build/recipe/session/framework/koa.js b/lib/build/recipe/session/framework/koa.js deleted file mode 100644 index 74b51cf3c..000000000 --- a/lib/build/recipe/session/framework/koa.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifySession = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipe_1 = __importDefault(require("../recipe")); -const framework_1 = require("../../../framework/koa/framework"); -function verifySession(options) { - return (ctx, next) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let request = new framework_1.KoaRequest(ctx); - let response = new framework_1.KoaResponse(ctx); - ctx.session = yield sessionRecipe.verifySession(options, request, response); - yield next(); - }); -} -exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/framework/loopback.d.ts b/lib/build/recipe/session/framework/loopback.d.ts deleted file mode 100644 index ee251753d..000000000 --- a/lib/build/recipe/session/framework/loopback.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { VerifySessionOptions } from ".."; -import { InterceptorOrKey } from "@loopback/core"; -export declare function verifySession(options?: VerifySessionOptions): InterceptorOrKey; diff --git a/lib/build/recipe/session/framework/loopback.js b/lib/build/recipe/session/framework/loopback.js deleted file mode 100644 index d43ae15d4..000000000 --- a/lib/build/recipe/session/framework/loopback.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifySession = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipe_1 = __importDefault(require("../recipe")); -const framework_1 = require("../../../framework/loopback/framework"); -function verifySession(options) { - return (ctx, next) => - __awaiter(this, void 0, void 0, function* () { - let sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - let middlewareCtx = yield ctx.get("middleware.http.context"); - let request = new framework_1.LoopbackRequest(middlewareCtx); - let response = new framework_1.LoopbackResponse(middlewareCtx); - middlewareCtx.session = yield sessionRecipe.verifySession(options, request, response); - return yield next(); - }); -} -exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/index.d.ts b/lib/build/recipe/session/index.d.ts deleted file mode 100644 index 6d1d48356..000000000 --- a/lib/build/recipe/session/index.d.ts +++ /dev/null @@ -1,191 +0,0 @@ -// @ts-nocheck -import SuperTokensError from "./error"; -import { - VerifySessionOptions, - RecipeInterface, - SessionContainerInterface as SessionContainer, - SessionInformation, - APIInterface, - APIOptions, - SessionClaimValidator, - SessionClaim, - ClaimValidationError, -} from "./types"; -import Recipe from "./recipe"; -import { JSONObject } from "../../types"; -export default class SessionWrapper { - static init: typeof Recipe.init; - static Error: typeof SuperTokensError; - static createNewSession( - req: any, - res: any, - userId: string, - accessTokenPayload?: any, - sessionData?: any, - userContext?: any - ): Promise; - static validateClaimsForSessionHandle( - sessionHandle: string, - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - sessionInfo: SessionInformation, - userContext: any - ) => Promise | SessionClaimValidator[], - userContext?: any - ): Promise< - | { - status: "SESSION_DOES_NOT_EXIST_ERROR"; - } - | { - status: "OK"; - invalidClaims: ClaimValidationError[]; - } - >; - static validateClaimsInJWTPayload( - userId: string, - jwtPayload: JSONObject, - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - userId: string, - userContext: any - ) => Promise | SessionClaimValidator[], - userContext?: any - ): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }>; - static getSession(req: any, res: any): Promise; - static getSession( - req: any, - res: any, - options?: VerifySessionOptions & { - sessionRequired?: true; - }, - userContext?: any - ): Promise; - static getSession( - req: any, - res: any, - options?: VerifySessionOptions & { - sessionRequired: false; - }, - userContext?: any - ): Promise; - static getSessionInformation(sessionHandle: string, userContext?: any): Promise; - static refreshSession(req: any, res: any, userContext?: any): Promise; - static revokeAllSessionsForUser(userId: string, userContext?: any): Promise; - static getAllSessionHandlesForUser(userId: string, userContext?: any): Promise; - static revokeSession(sessionHandle: string, userContext?: any): Promise; - static revokeMultipleSessions(sessionHandles: string[], userContext?: any): Promise; - static updateSessionData(sessionHandle: string, newSessionData: any, userContext?: any): Promise; - static regenerateAccessToken( - accessToken: string, - newAccessTokenPayload?: any, - userContext?: any - ): Promise< - | { - status: "OK"; - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken?: - | { - token: string; - expiry: number; - createdTime: number; - } - | undefined; - } - | undefined - >; - static updateAccessTokenPayload( - sessionHandle: string, - newAccessTokenPayload: any, - userContext?: any - ): Promise; - static mergeIntoAccessTokenPayload( - sessionHandle: string, - accessTokenPayloadUpdate: JSONObject, - userContext?: any - ): Promise; - static createJWT( - payload?: any, - validitySeconds?: number, - userContext?: any - ): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - static getJWKS( - userContext?: any - ): Promise<{ - status: "OK"; - keys: import("../jwt").JsonWebKey[]; - }>; - static getOpenIdDiscoveryConfiguration( - userContext?: any - ): Promise<{ - status: "OK"; - issuer: string; - jwks_uri: string; - }>; - static fetchAndSetClaim(sessionHandle: string, claim: SessionClaim, userContext?: any): Promise; - static setClaimValue( - sessionHandle: string, - claim: SessionClaim, - value: T, - userContext?: any - ): Promise; - static getClaimValue( - sessionHandle: string, - claim: SessionClaim, - userContext?: any - ): Promise< - | { - status: "SESSION_DOES_NOT_EXIST_ERROR"; - } - | { - status: "OK"; - value: T | undefined; - } - >; - static removeClaim(sessionHandle: string, claim: SessionClaim, userContext?: any): Promise; -} -export declare let init: typeof Recipe.init; -export declare let createNewSession: typeof SessionWrapper.createNewSession; -export declare let getSession: typeof SessionWrapper.getSession; -export declare let getSessionInformation: typeof SessionWrapper.getSessionInformation; -export declare let refreshSession: typeof SessionWrapper.refreshSession; -export declare let revokeAllSessionsForUser: typeof SessionWrapper.revokeAllSessionsForUser; -export declare let getAllSessionHandlesForUser: typeof SessionWrapper.getAllSessionHandlesForUser; -export declare let revokeSession: typeof SessionWrapper.revokeSession; -export declare let revokeMultipleSessions: typeof SessionWrapper.revokeMultipleSessions; -export declare let updateSessionData: typeof SessionWrapper.updateSessionData; -export declare let updateAccessTokenPayload: typeof SessionWrapper.updateAccessTokenPayload; -export declare let mergeIntoAccessTokenPayload: typeof SessionWrapper.mergeIntoAccessTokenPayload; -export declare let fetchAndSetClaim: typeof SessionWrapper.fetchAndSetClaim; -export declare let setClaimValue: typeof SessionWrapper.setClaimValue; -export declare let getClaimValue: typeof SessionWrapper.getClaimValue; -export declare let removeClaim: typeof SessionWrapper.removeClaim; -export declare let validateClaimsInJWTPayload: typeof SessionWrapper.validateClaimsInJWTPayload; -export declare let validateClaimsForSessionHandle: typeof SessionWrapper.validateClaimsForSessionHandle; -export declare let Error: typeof SuperTokensError; -export declare let createJWT: typeof SessionWrapper.createJWT; -export declare let getJWKS: typeof SessionWrapper.getJWKS; -export declare let getOpenIdDiscoveryConfiguration: typeof SessionWrapper.getOpenIdDiscoveryConfiguration; -export type { - VerifySessionOptions, - RecipeInterface, - SessionContainer, - APIInterface, - APIOptions, - SessionInformation, - SessionClaimValidator, -}; diff --git a/lib/build/recipe/session/index.js b/lib/build/recipe/session/index.js deleted file mode 100644 index 0171672c2..000000000 --- a/lib/build/recipe/session/index.js +++ /dev/null @@ -1,326 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getOpenIdDiscoveryConfiguration = exports.getJWKS = exports.createJWT = exports.Error = exports.validateClaimsForSessionHandle = exports.validateClaimsInJWTPayload = exports.removeClaim = exports.getClaimValue = exports.setClaimValue = exports.fetchAndSetClaim = exports.mergeIntoAccessTokenPayload = exports.updateAccessTokenPayload = exports.updateSessionData = exports.revokeMultipleSessions = exports.revokeSession = exports.getAllSessionHandlesForUser = exports.revokeAllSessionsForUser = exports.refreshSession = exports.getSessionInformation = exports.getSession = exports.createNewSession = exports.init = void 0; -const error_1 = __importDefault(require("./error")); -const recipe_1 = __importDefault(require("./recipe")); -const framework_1 = __importDefault(require("../../framework")); -const supertokens_1 = __importDefault(require("../../supertokens")); -const utils_1 = require("./utils"); -// For Express -class SessionWrapper { - static createNewSession(req, res, userId, accessTokenPayload = {}, sessionData = {}, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - const claimsAddedByOtherRecipes = recipe_1.default.getInstanceOrThrowError().getClaimsAddedByOtherRecipes(); - let finalAccessTokenPayload = accessTokenPayload; - for (const claim of claimsAddedByOtherRecipes) { - const update = yield claim.build(userId, userContext); - finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); - } - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); - } - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); - } - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewSession({ - req, - res, - userId, - accessTokenPayload: finalAccessTokenPayload, - sessionData, - userContext, - }); - }); - } - static validateClaimsForSessionHandle(sessionHandle, overrideGlobalClaimValidators, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - const recipeImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const sessionInfo = yield recipeImpl.getSessionInformation({ - sessionHandle, - userContext, - }); - if (sessionInfo === undefined) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - const claimValidatorsAddedByOtherRecipes = recipe_1.default - .getInstanceOrThrowError() - .getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators = yield recipeImpl.getGlobalClaimValidators({ - userId: sessionInfo === null || sessionInfo === void 0 ? void 0 : sessionInfo.userId, - claimValidatorsAddedByOtherRecipes, - userContext, - }); - const claimValidators = - overrideGlobalClaimValidators !== undefined - ? yield overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, userContext) - : globalClaimValidators; - let claimValidationResponse = yield recipeImpl.validateClaims({ - userId: sessionInfo.userId, - accessTokenPayload: sessionInfo.accessTokenPayload, - claimValidators, - userContext, - }); - if (claimValidationResponse.accessTokenPayloadUpdate !== undefined) { - if ( - !(yield recipeImpl.mergeIntoAccessTokenPayload({ - sessionHandle, - accessTokenPayloadUpdate: claimValidationResponse.accessTokenPayloadUpdate, - userContext, - })) - ) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - } - return { - status: "OK", - invalidClaims: claimValidationResponse.invalidClaims, - }; - }); - } - static validateClaimsInJWTPayload(userId, jwtPayload, overrideGlobalClaimValidators, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - const recipeImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const claimValidatorsAddedByOtherRecipes = recipe_1.default - .getInstanceOrThrowError() - .getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators = yield recipeImpl.getGlobalClaimValidators({ - userId, - claimValidatorsAddedByOtherRecipes, - userContext, - }); - const claimValidators = - overrideGlobalClaimValidators !== undefined - ? yield overrideGlobalClaimValidators(globalClaimValidators, userId, userContext) - : globalClaimValidators; - return recipeImpl.validateClaimsInJWTPayload({ - userId, - jwtPayload, - claimValidators, - userContext, - }); - }); - } - static getSession(req, res, options, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); - } - const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; - const session = yield recipeInterfaceImpl.getSession({ req, res, options, userContext }); - if (session !== undefined) { - const claimValidators = yield utils_1.getRequiredClaimValidators( - session, - options === null || options === void 0 ? void 0 : options.overrideGlobalClaimValidators, - userContext - ); - yield session.assertClaims(claimValidators, userContext); - } - return session; - }); - } - static getSessionInformation(sessionHandle, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getSessionInformation({ - sessionHandle, - userContext, - }); - } - static refreshSession(req, res, userContext = {}) { - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); - } - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.refreshSession({ req, res, userContext }); - } - static revokeAllSessionsForUser(userId, userContext = {}) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeAllSessionsForUser({ userId, userContext }); - } - static getAllSessionHandlesForUser(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getAllSessionHandlesForUser({ - userId, - userContext, - }); - } - static revokeSession(sessionHandle, userContext = {}) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeSession({ sessionHandle, userContext }); - } - static revokeMultipleSessions(sessionHandles, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeMultipleSessions({ - sessionHandles, - userContext, - }); - } - static updateSessionData(sessionHandle, newSessionData, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateSessionData({ - sessionHandle, - newSessionData, - userContext, - }); - } - static regenerateAccessToken(accessToken, newAccessTokenPayload, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.regenerateAccessToken({ - accessToken, - newAccessTokenPayload, - userContext, - }); - } - static updateAccessTokenPayload(sessionHandle, newAccessTokenPayload, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateAccessTokenPayload({ - sessionHandle, - newAccessTokenPayload, - userContext, - }); - } - static mergeIntoAccessTokenPayload(sessionHandle, accessTokenPayloadUpdate, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.mergeIntoAccessTokenPayload({ - sessionHandle, - accessTokenPayloadUpdate, - userContext, - }); - } - static createJWT(payload, validitySeconds, userContext = {}) { - let openIdRecipe = recipe_1.default.getInstanceOrThrowError().openIdRecipe; - if (openIdRecipe !== undefined) { - return openIdRecipe.recipeImplementation.createJWT({ payload, validitySeconds, userContext }); - } - throw new global.Error( - "createJWT cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ); - } - static getJWKS(userContext = {}) { - let openIdRecipe = recipe_1.default.getInstanceOrThrowError().openIdRecipe; - if (openIdRecipe !== undefined) { - return openIdRecipe.recipeImplementation.getJWKS({ userContext }); - } - throw new global.Error( - "getJWKS cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ); - } - static getOpenIdDiscoveryConfiguration(userContext = {}) { - let openIdRecipe = recipe_1.default.getInstanceOrThrowError().openIdRecipe; - if (openIdRecipe !== undefined) { - return openIdRecipe.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); - } - throw new global.Error( - "getOpenIdDiscoveryConfiguration cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ); - } - static fetchAndSetClaim(sessionHandle, claim, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.fetchAndSetClaim({ - sessionHandle, - claim, - userContext, - }); - } - static setClaimValue(sessionHandle, claim, value, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.setClaimValue({ - sessionHandle, - claim, - value, - userContext, - }); - } - static getClaimValue(sessionHandle, claim, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getClaimValue({ - sessionHandle, - claim, - userContext, - }); - } - static removeClaim(sessionHandle, claim, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removeClaim({ - sessionHandle, - claim, - userContext, - }); - } -} -exports.default = SessionWrapper; -SessionWrapper.init = recipe_1.default.init; -SessionWrapper.Error = error_1.default; -exports.init = SessionWrapper.init; -exports.createNewSession = SessionWrapper.createNewSession; -exports.getSession = SessionWrapper.getSession; -exports.getSessionInformation = SessionWrapper.getSessionInformation; -exports.refreshSession = SessionWrapper.refreshSession; -exports.revokeAllSessionsForUser = SessionWrapper.revokeAllSessionsForUser; -exports.getAllSessionHandlesForUser = SessionWrapper.getAllSessionHandlesForUser; -exports.revokeSession = SessionWrapper.revokeSession; -exports.revokeMultipleSessions = SessionWrapper.revokeMultipleSessions; -exports.updateSessionData = SessionWrapper.updateSessionData; -exports.updateAccessTokenPayload = SessionWrapper.updateAccessTokenPayload; -exports.mergeIntoAccessTokenPayload = SessionWrapper.mergeIntoAccessTokenPayload; -exports.fetchAndSetClaim = SessionWrapper.fetchAndSetClaim; -exports.setClaimValue = SessionWrapper.setClaimValue; -exports.getClaimValue = SessionWrapper.getClaimValue; -exports.removeClaim = SessionWrapper.removeClaim; -exports.validateClaimsInJWTPayload = SessionWrapper.validateClaimsInJWTPayload; -exports.validateClaimsForSessionHandle = SessionWrapper.validateClaimsForSessionHandle; -exports.Error = SessionWrapper.Error; -// JWT Functions -exports.createJWT = SessionWrapper.createJWT; -exports.getJWKS = SessionWrapper.getJWKS; -// Open id functions -exports.getOpenIdDiscoveryConfiguration = SessionWrapper.getOpenIdDiscoveryConfiguration; diff --git a/lib/build/recipe/session/jwt.d.ts b/lib/build/recipe/session/jwt.d.ts deleted file mode 100644 index 4dda635b3..000000000 --- a/lib/build/recipe/session/jwt.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -export declare type ParsedJWTInfo = { - rawTokenString: string; - rawPayload: string; - header: string; - payload: any; - signature: string; -}; -export declare function parseJWTWithoutSignatureVerification(jwt: string): ParsedJWTInfo; -export declare function verifyJWT({ header, rawPayload, signature }: ParsedJWTInfo, jwtSigningPublicKey: string): void; diff --git a/lib/build/recipe/session/jwt.js b/lib/build/recipe/session/jwt.js deleted file mode 100644 index d13a9c7ad..000000000 --- a/lib/build/recipe/session/jwt.js +++ /dev/null @@ -1,105 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifyJWT = exports.parseJWTWithoutSignatureVerification = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const crypto = __importStar(require("crypto")); -const HEADERS = new Set([ - Buffer.from( - JSON.stringify({ - alg: "RS256", - typ: "JWT", - version: "1", - }) - ).toString("base64"), - Buffer.from( - JSON.stringify({ - alg: "RS256", - typ: "JWT", - version: "2", - }) - ).toString("base64"), -]); -function parseJWTWithoutSignatureVerification(jwt) { - const splittedInput = jwt.split("."); - if (splittedInput.length !== 3) { - throw new Error("Invalid JWT"); - } - // checking header - if (!HEADERS.has(splittedInput[0])) { - throw new Error("JWT header mismatch"); - } - return { - rawTokenString: jwt, - rawPayload: splittedInput[1], - header: splittedInput[0], - // Ideally we would only parse this after the signature verification is done. - // We do this at the start, since we want to check if a token can be a supertokens access token or not - payload: JSON.parse(Buffer.from(splittedInput[1], "base64").toString()), - signature: splittedInput[2], - }; -} -exports.parseJWTWithoutSignatureVerification = parseJWTWithoutSignatureVerification; -function verifyJWT({ header, rawPayload, signature }, jwtSigningPublicKey) { - let verifier = crypto.createVerify("sha256"); - // convert the jwtSigningPublicKey into .pem format - verifier.update(header + "." + rawPayload); - if ( - !verifier.verify( - "-----BEGIN PUBLIC KEY-----\n" + jwtSigningPublicKey + "\n-----END PUBLIC KEY-----", - signature, - "base64" - ) - ) { - throw new Error("JWT verification failed"); - } -} -exports.verifyJWT = verifyJWT; diff --git a/lib/build/recipe/session/recipe.d.ts b/lib/build/recipe/session/recipe.d.ts deleted file mode 100644 index a7d920c95..000000000 --- a/lib/build/recipe/session/recipe.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { - TypeInput, - TypeNormalisedInput, - RecipeInterface, - APIInterface, - VerifySessionOptions, - SessionClaimValidator, - SessionClaim, -} from "./types"; -import STError from "./error"; -import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod } from "../../types"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import OpenIdRecipe from "../openid/recipe"; -export default class SessionRecipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - private claimsAddedByOtherRecipes; - private claimValidatorsAddedByOtherRecipes; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - openIdRecipe?: OpenIdRecipe; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); - static getInstanceOrThrowError(): SessionRecipe; - static init(config?: TypeInput): RecipeListFunction; - static reset(): void; - addClaimFromOtherRecipe: (claim: SessionClaim) => void; - getClaimsAddedByOtherRecipes: () => SessionClaim[]; - addClaimValidatorFromOtherRecipe: (builder: SessionClaimValidator) => void; - getClaimValidatorsAddedByOtherRecipes: () => SessionClaimValidator[]; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ) => Promise; - handleError: (err: STError, request: BaseRequest, response: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; - verifySession: ( - options: VerifySessionOptions | undefined, - request: BaseRequest, - response: BaseResponse - ) => Promise; -} diff --git a/lib/build/recipe/session/recipe.js b/lib/build/recipe/session/recipe.js deleted file mode 100644 index af88b2ba8..000000000 --- a/lib/build/recipe/session/recipe.js +++ /dev/null @@ -1,280 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const error_1 = __importDefault(require("./error")); -const utils_1 = require("./utils"); -const refresh_1 = __importDefault(require("./api/refresh")); -const signout_1 = __importDefault(require("./api/signout")); -const constants_1 = require("./constants"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const cookieAndHeaders_1 = require("./cookieAndHeaders"); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const with_jwt_1 = __importDefault(require("./with-jwt")); -const querier_1 = require("../../querier"); -const implementation_1 = __importDefault(require("./api/implementation")); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const recipe_1 = __importDefault(require("../openid/recipe")); -const logger_1 = require("../../logger"); -const utils_2 = require("../../utils"); -// For Express -class SessionRecipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config) { - super(recipeId, appInfo); - this.claimsAddedByOtherRecipes = []; - this.claimValidatorsAddedByOtherRecipes = []; - this.addClaimFromOtherRecipe = (claim) => { - // We are throwing here (and not in addClaimValidatorFromOtherRecipe) because if multiple - // claims are added with the same key they will overwrite each other. Validators will all run - // and work as expected even if they are added multiple times. - if (this.claimsAddedByOtherRecipes.some((c) => c.key === claim.key)) { - throw new Error("Claim added by multiple recipes"); - } - this.claimsAddedByOtherRecipes.push(claim); - }; - this.getClaimsAddedByOtherRecipes = () => { - return this.claimsAddedByOtherRecipes; - }; - this.addClaimValidatorFromOtherRecipe = (builder) => { - this.claimValidatorsAddedByOtherRecipes.push(builder); - }; - this.getClaimValidatorsAddedByOtherRecipes = () => { - return this.claimValidatorsAddedByOtherRecipes; - }; - // abstract instance functions below............... - this.getAPIsHandled = () => { - let apisHandled = [ - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.REFRESH_API_PATH), - id: constants_1.REFRESH_API_PATH, - disabled: this.apiImpl.refreshPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGNOUT_API_PATH), - id: constants_1.SIGNOUT_API_PATH, - disabled: this.apiImpl.signOutPOST === undefined, - }, - ]; - if (this.openIdRecipe !== undefined) { - apisHandled.push(...this.openIdRecipe.getAPIsHandled()); - } - return apisHandled; - }; - this.handleAPIRequest = (id, req, res, path, method) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - }; - if (id === constants_1.REFRESH_API_PATH) { - return yield refresh_1.default(this.apiImpl, options); - } else if (id === constants_1.SIGNOUT_API_PATH) { - return yield signout_1.default(this.apiImpl, options); - } else if (this.openIdRecipe !== undefined) { - return yield this.openIdRecipe.handleAPIRequest(id, req, res, path, method); - } else { - return false; - } - }); - this.handleError = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === SessionRecipe.RECIPE_ID) { - if (err.type === error_1.default.UNAUTHORISED) { - logger_1.logDebugMessage("errorHandler: returning UNAUTHORISED"); - if ( - err.payload === undefined || - err.payload.clearTokens === undefined || - err.payload.clearTokens === true - ) { - logger_1.logDebugMessage("errorHandler: Clearing tokens because of UNAUTHORISED response"); - cookieAndHeaders_1.clearSessionFromAllTokenTransferMethods(this.config, response); - } - return yield this.config.errorHandlers.onUnauthorised(err.message, request, response); - } else if (err.type === error_1.default.TRY_REFRESH_TOKEN) { - logger_1.logDebugMessage("errorHandler: returning TRY_REFRESH_TOKEN"); - return yield this.config.errorHandlers.onTryRefreshToken(err.message, request, response); - } else if (err.type === error_1.default.TOKEN_THEFT_DETECTED) { - logger_1.logDebugMessage("errorHandler: returning TOKEN_THEFT_DETECTED"); - logger_1.logDebugMessage( - "errorHandler: Clearing tokens because of TOKEN_THEFT_DETECTED response" - ); - cookieAndHeaders_1.clearSessionFromAllTokenTransferMethods(this.config, response); - return yield this.config.errorHandlers.onTokenTheftDetected( - err.payload.sessionHandle, - err.payload.userId, - request, - response - ); - } else if (err.type === error_1.default.INVALID_CLAIMS) { - return yield this.config.errorHandlers.onInvalidClaim(err.payload, request, response); - } else { - throw err; - } - } else if (this.openIdRecipe !== undefined) { - return yield this.openIdRecipe.handleError(err, request, response); - } else { - throw err; - } - }); - this.getAllCORSHeaders = () => { - let corsHeaders = [...cookieAndHeaders_1.getCORSAllowedHeaders()]; - if (this.openIdRecipe !== undefined) { - corsHeaders.push(...this.openIdRecipe.getAllCORSHeaders()); - } - return corsHeaders; - }; - this.isErrorFromThisRecipe = (err) => { - return ( - error_1.default.isErrorFromSuperTokens(err) && - (err.fromRecipe === SessionRecipe.RECIPE_ID || - (this.openIdRecipe !== undefined && this.openIdRecipe.isErrorFromThisRecipe(err))) - ); - }; - this.verifySession = (options, request, response) => - __awaiter(this, void 0, void 0, function* () { - return yield this.apiImpl.verifySession({ - verifySessionOptions: options, - options: { - config: this.config, - req: request, - res: response, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - }, - userContext: utils_2.makeDefaultUserContextFromAPI(request), - }); - }); - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - logger_1.logDebugMessage("session init: antiCsrf: " + this.config.antiCsrf); - logger_1.logDebugMessage("session init: cookieDomain: " + this.config.cookieDomain); - logger_1.logDebugMessage("session init: cookieSameSite: " + this.config.cookieSameSite); - logger_1.logDebugMessage("session init: cookieSecure: " + this.config.cookieSecure); - logger_1.logDebugMessage( - "session init: refreshTokenPath: " + this.config.refreshTokenPath.getAsStringDangerous() - ); - logger_1.logDebugMessage("session init: sessionExpiredStatusCode: " + this.config.sessionExpiredStatusCode); - this.isInServerlessEnv = isInServerlessEnv; - if (this.config.jwt.enable === true) { - this.openIdRecipe = new recipe_1.default(recipeId, appInfo, isInServerlessEnv, { - issuer: this.config.jwt.issuer, - override: this.config.override.openIdFeature, - }); - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default( - querier_1.Querier.getNewInstanceOrThrowError(recipeId), - this.config, - this.getAppInfo(), - () => this.recipeInterfaceImpl - ) - ); - this.recipeInterfaceImpl = builder - .override((oI) => { - return with_jwt_1.default( - oI, - // this.jwtRecipe is never undefined here - this.openIdRecipe.recipeImplementation, - this.config - ); - }) - .override(this.config.override.functions) - .build(); - } else { - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default( - querier_1.Querier.getNewInstanceOrThrowError(recipeId), - this.config, - this.getAppInfo(), - () => this.recipeInterfaceImpl - ) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - static getInstanceOrThrowError() { - if (SessionRecipe.instance !== undefined) { - return SessionRecipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (SessionRecipe.instance === undefined) { - SessionRecipe.instance = new SessionRecipe(SessionRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return SessionRecipe.instance; - } else { - throw new Error("Session recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - SessionRecipe.instance = undefined; - } -} -exports.default = SessionRecipe; -SessionRecipe.instance = undefined; -SessionRecipe.RECIPE_ID = "session"; diff --git a/lib/build/recipe/session/recipeImplementation.d.ts b/lib/build/recipe/session/recipeImplementation.d.ts deleted file mode 100644 index bbca23737..000000000 --- a/lib/build/recipe/session/recipeImplementation.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck -import { RecipeInterface, TypeNormalisedInput, KeyInfo, AntiCsrfType } from "./types"; -import { Querier } from "../../querier"; -import { NormalisedAppinfo } from "../../types"; -export declare class HandshakeInfo { - antiCsrf: AntiCsrfType; - accessTokenBlacklistingEnabled: boolean; - accessTokenValidity: number; - refreshTokenValidity: number; - private rawJwtSigningPublicKeyList; - constructor( - antiCsrf: AntiCsrfType, - accessTokenBlacklistingEnabled: boolean, - accessTokenValidity: number, - refreshTokenValidity: number, - rawJwtSigningPublicKeyList: KeyInfo[] - ); - setJwtSigningPublicKeyList(updatedList: KeyInfo[]): void; - getJwtSigningPublicKeyList(): KeyInfo[]; - clone(): HandshakeInfo; -} -export declare type Helpers = { - querier: Querier; - getHandshakeInfo: (forceRefetch?: boolean) => Promise; - updateJwtSigningPublicKeyInfo: (keyList: KeyInfo[] | undefined, publicKey: string, expiryTime: number) => void; - config: TypeNormalisedInput; - appInfo: NormalisedAppinfo; - getRecipeImpl: () => RecipeInterface; -}; -export default function getRecipeInterface( - querier: Querier, - config: TypeNormalisedInput, - appInfo: NormalisedAppinfo, - getRecipeImplAfterOverrides: () => RecipeInterface -): RecipeInterface; diff --git a/lib/build/recipe/session/recipeImplementation.js b/lib/build/recipe/session/recipeImplementation.js deleted file mode 100644 index ab46d7348..000000000 --- a/lib/build/recipe/session/recipeImplementation.js +++ /dev/null @@ -1,677 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.HandshakeInfo = void 0; -const SessionFunctions = __importStar(require("./sessionFunctions")); -const cookieAndHeaders_1 = require("./cookieAndHeaders"); -const utils_1 = require("./utils"); -const sessionClass_1 = __importDefault(require("./sessionClass")); -const error_1 = __importDefault(require("./error")); -const utils_2 = require("../../utils"); -const processState_1 = require("../../processState"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const logger_1 = require("../../logger"); -const constants_1 = require("./constants"); -const jwt_1 = require("./jwt"); -const accessToken_1 = require("./accessToken"); -class HandshakeInfo { - constructor( - antiCsrf, - accessTokenBlacklistingEnabled, - accessTokenValidity, - refreshTokenValidity, - rawJwtSigningPublicKeyList - ) { - this.antiCsrf = antiCsrf; - this.accessTokenBlacklistingEnabled = accessTokenBlacklistingEnabled; - this.accessTokenValidity = accessTokenValidity; - this.refreshTokenValidity = refreshTokenValidity; - this.rawJwtSigningPublicKeyList = rawJwtSigningPublicKeyList; - } - setJwtSigningPublicKeyList(updatedList) { - this.rawJwtSigningPublicKeyList = updatedList; - } - getJwtSigningPublicKeyList() { - return this.rawJwtSigningPublicKeyList.filter((key) => key.expiryTime > Date.now()); - } - clone() { - return new HandshakeInfo( - this.antiCsrf, - this.accessTokenBlacklistingEnabled, - this.accessTokenValidity, - this.refreshTokenValidity, - this.rawJwtSigningPublicKeyList - ); - } -} -exports.HandshakeInfo = HandshakeInfo; -// We are defining this here to reduce the scope of legacy code -const LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken"; -function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverrides) { - let handshakeInfo; - function getHandshakeInfo(forceRefetch = false) { - return __awaiter(this, void 0, void 0, function* () { - if ( - handshakeInfo === undefined || - handshakeInfo.getJwtSigningPublicKeyList().length === 0 || - forceRefetch - ) { - let antiCsrf = config.antiCsrf; - processState_1.ProcessState.getInstance().addState( - processState_1.PROCESS_STATE.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO - ); - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/handshake"), {}); - handshakeInfo = new HandshakeInfo( - antiCsrf, - response.accessTokenBlacklistingEnabled, - response.accessTokenValidity, - response.refreshTokenValidity, - response.jwtSigningPublicKeyList - ); - updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - } - return handshakeInfo; - }); - } - function updateJwtSigningPublicKeyInfo(keyList, publicKey, expiryTime) { - if (keyList === undefined) { - // Setting createdAt to Date.now() emulates the old lastUpdatedAt logic - keyList = [{ publicKey, expiryTime, createdAt: Date.now() }]; - } - if (handshakeInfo !== undefined) { - handshakeInfo.setJwtSigningPublicKeyList(keyList); - } - } - let obj = { - createNewSession: function ({ req, res, userId, accessTokenPayload = {}, sessionData = {}, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("createNewSession: Started"); - let outputTransferMethod = config.getTokenTransferMethod({ - req, - forCreateNewSession: true, - userContext, - }); - if (outputTransferMethod === "any") { - outputTransferMethod = "header"; - } - logger_1.logDebugMessage("createNewSession: using transfer method " + outputTransferMethod); - if ( - outputTransferMethod === "cookie" && - helpers.config.cookieSameSite === "none" && - !helpers.config.cookieSecure && - !( - (helpers.appInfo.topLevelAPIDomain === "localhost" || - utils_2.isAnIpAddress(helpers.appInfo.topLevelAPIDomain)) && - (helpers.appInfo.topLevelWebsiteDomain === "localhost" || - utils_2.isAnIpAddress(helpers.appInfo.topLevelWebsiteDomain)) - ) - ) { - // We can allow insecure cookie when both website & API domain are localhost or an IP - // When either of them is a different domain, API domain needs to have https and a secure cookie to work - throw new Error( - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." - ); - } - const disableAntiCSRF = outputTransferMethod === "header"; - let response = yield SessionFunctions.createNewSession( - helpers, - userId, - disableAntiCSRF, - accessTokenPayload, - sessionData - ); - for (const transferMethod of constants_1.availableTokenTransferMethods) { - if ( - transferMethod !== outputTransferMethod && - cookieAndHeaders_1.getToken(req, "access", transferMethod) !== undefined - ) { - cookieAndHeaders_1.clearSession(config, res, transferMethod); - } - } - utils_1.attachTokensToResponse(config, res, response, outputTransferMethod); - return new sessionClass_1.default( - helpers, - response.accessToken.token, - response.session.handle, - response.session.userId, - response.session.userDataInJWT, - res, - req, - outputTransferMethod - ); - }); - }, - getGlobalClaimValidators: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return input.claimValidatorsAddedByOtherRecipes; - }); - }, - /* In all cases if sIdRefreshToken token exists (so it's a legacy session) we return TRY_REFRESH_TOKEN. The refresh endpoint will clear this cookie and try to upgrade the session. - Check https://supertokens.com/docs/contribute/decisions/session/0007 for further details and a table of expected behaviours - */ - getSession: function ({ req, res, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("getSession: Started"); - // This token isn't handled by getToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - // This could create a spike on refresh calls during the update of the backend SDK - throw new error_1.default({ - message: "using legacy session, please call the refresh API", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - const sessionOptional = - (options === null || options === void 0 ? void 0 : options.sessionRequired) === false; - logger_1.logDebugMessage("getSession: optional validation: " + sessionOptional); - const accessTokens = {}; - // We check all token transfer methods for available access tokens - for (const transferMethod of constants_1.availableTokenTransferMethods) { - const tokenString = cookieAndHeaders_1.getToken(req, "access", transferMethod); - if (tokenString !== undefined) { - try { - const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString); - accessToken_1.validateAccessTokenStructure(info.payload); - logger_1.logDebugMessage("getSession: got access token from " + transferMethod); - accessTokens[transferMethod] = info; - } catch (_a) { - logger_1.logDebugMessage( - `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` - ); - } - } - } - const allowedTransferMethod = config.getTokenTransferMethod({ - req, - forCreateNewSession: false, - userContext, - }); - let requestTransferMethod; - let accessToken; - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - accessTokens["header"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using header transfer method"); - requestTransferMethod = "header"; - accessToken = accessTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - accessTokens["cookie"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using cookie transfer method"); - requestTransferMethod = "cookie"; - accessToken = accessTokens["cookie"]; - } else { - if (sessionOptional) { - logger_1.logDebugMessage( - "getSession: returning undefined because accessToken is undefined and sessionRequired is false" - ); - // there is no session that exists here, and the user wants session verification - // to be optional. So we return undefined. - return undefined; - } - logger_1.logDebugMessage("getSession: UNAUTHORISED because accessToken in request is undefined"); - throw new error_1.default({ - message: - "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", - type: error_1.default.UNAUTHORISED, - payload: { - // we do not clear the session here because of a - // race condition mentioned here: https://github.com/supertokens/supertokens-node/issues/17 - clearTokens: false, - }, - }); - } - let antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); - let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; - if (doAntiCsrfCheck === undefined) { - doAntiCsrfCheck = utils_2.normaliseHttpMethod(req.getMethod()) !== "get"; - } - if (requestTransferMethod === "header") { - doAntiCsrfCheck = false; - } - logger_1.logDebugMessage("getSession: Value of doAntiCsrfCheck is: " + doAntiCsrfCheck); - let response = yield SessionFunctions.getSession( - helpers, - accessToken, - antiCsrfToken, - doAntiCsrfCheck, - utils_2.getRidFromHeader(req) !== undefined - ); - let accessTokenString = accessToken.rawTokenString; - if (response.accessToken !== undefined) { - cookieAndHeaders_1.setFrontTokenInHeaders( - res, - response.session.userId, - response.accessToken.expiry, - response.session.userDataInJWT - ); - cookieAndHeaders_1.setToken( - config, - res, - "access", - response.accessToken.token, - // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. - // Even if the token is expired the presence of the token indicates that the user could have a valid refresh - // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. - Date.now() + 3153600000000, - requestTransferMethod - ); - accessTokenString = response.accessToken.token; - } - logger_1.logDebugMessage("getSession: Success!"); - const session = new sessionClass_1.default( - helpers, - accessTokenString, - response.session.handle, - response.session.userId, - response.session.userDataInJWT, - res, - req, - requestTransferMethod - ); - return session; - }); - }, - validateClaims: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let accessTokenPayload = input.accessTokenPayload; - let accessTokenPayloadUpdate = undefined; - const origSessionClaimPayloadJSON = JSON.stringify(accessTokenPayload); - for (const validator of input.claimValidators) { - logger_1.logDebugMessage( - "updateClaimsInPayloadIfNeeded checking shouldRefetch for " + validator.id - ); - if ( - "claim" in validator && - (yield validator.shouldRefetch(accessTokenPayload, input.userContext)) - ) { - logger_1.logDebugMessage("updateClaimsInPayloadIfNeeded refetching " + validator.id); - const value = yield validator.claim.fetchValue(input.userId, input.userContext); - logger_1.logDebugMessage( - "updateClaimsInPayloadIfNeeded " + validator.id + " refetch result " + JSON.stringify(value) - ); - if (value !== undefined) { - accessTokenPayload = validator.claim.addToPayload_internal( - accessTokenPayload, - value, - input.userContext - ); - } - } - } - if (JSON.stringify(accessTokenPayload) !== origSessionClaimPayloadJSON) { - accessTokenPayloadUpdate = accessTokenPayload; - } - const invalidClaims = yield utils_1.validateClaimsInPayload( - input.claimValidators, - accessTokenPayload, - input.userContext - ); - return { - invalidClaims, - accessTokenPayloadUpdate, - }; - }); - }, - validateClaimsInJWTPayload: function (input) { - return __awaiter(this, void 0, void 0, function* () { - // We skip refetching here, because we have no way of updating the JWT payload here - // if we have access to the entire session other methods can be used to do validation while updating - const invalidClaims = yield utils_1.validateClaimsInPayload( - input.claimValidators, - input.jwtPayload, - input.userContext - ); - return { - status: "OK", - invalidClaims, - }; - }); - }, - getSessionInformation: function ({ sessionHandle }) { - return __awaiter(this, void 0, void 0, function* () { - return SessionFunctions.getSessionInformation(helpers, sessionHandle); - }); - }, - /* - In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. - Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours - */ - refreshSession: function ({ req, res, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("refreshSession: Started"); - const refreshTokens = {}; - // We check all token transfer methods for available refresh tokens - // We do this so that we can later clear all we are not overwriting - for (const transferMethod of constants_1.availableTokenTransferMethods) { - refreshTokens[transferMethod] = cookieAndHeaders_1.getToken(req, "refresh", transferMethod); - if (refreshTokens[transferMethod] !== undefined) { - logger_1.logDebugMessage("refreshSession: got refresh token from " + transferMethod); - } - } - const allowedTransferMethod = config.getTokenTransferMethod({ - req, - forCreateNewSession: false, - userContext, - }); - logger_1.logDebugMessage("refreshSession: getTokenTransferMethod returned " + allowedTransferMethod); - let requestTransferMethod; - let refreshToken; - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - refreshTokens["header"] !== undefined - ) { - logger_1.logDebugMessage("refreshSession: using header transfer method"); - requestTransferMethod = "header"; - refreshToken = refreshTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - refreshTokens["cookie"] - ) { - logger_1.logDebugMessage("refreshSession: using cookie transfer method"); - requestTransferMethod = "cookie"; - refreshToken = refreshTokens["cookie"]; - } else { - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logger_1.logDebugMessage( - "refreshSession: cleared legacy id refresh token because refresh token was not found" - ); - cookieAndHeaders_1.setCookie( - config, - res, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "accessTokenPath" - ); - } - logger_1.logDebugMessage( - "refreshSession: UNAUTHORISED because refresh token in request is undefined" - ); - throw new error_1.default({ - message: "Refresh token not found. Are you sending the refresh token in the request?", - payload: { - clearTokens: false, - }, - type: error_1.default.UNAUTHORISED, - }); - } - try { - let antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); - let response = yield SessionFunctions.refreshSession( - helpers, - refreshToken, - antiCsrfToken, - utils_2.getRidFromHeader(req) !== undefined, - requestTransferMethod - ); - logger_1.logDebugMessage( - "refreshSession: Attaching refreshed session info as " + requestTransferMethod - ); - // We clear the tokens in all token transfer methods we are not going to overwrite - for (const transferMethod of constants_1.availableTokenTransferMethods) { - if (transferMethod !== requestTransferMethod && refreshTokens[transferMethod] !== undefined) { - cookieAndHeaders_1.clearSession(config, res, transferMethod); - } - } - utils_1.attachTokensToResponse(config, res, response, requestTransferMethod); - logger_1.logDebugMessage("refreshSession: Success!"); - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logger_1.logDebugMessage( - "refreshSession: cleared legacy id refresh token after successful refresh" - ); - cookieAndHeaders_1.setCookie( - config, - res, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "accessTokenPath" - ); - } - return new sessionClass_1.default( - helpers, - response.accessToken.token, - response.session.handle, - response.session.userId, - response.session.userDataInJWT, - res, - req, - requestTransferMethod - ); - } catch (err) { - if (err.type === error_1.default.TOKEN_THEFT_DETECTED || err.payload.clearTokens) { - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logger_1.logDebugMessage( - "refreshSession: cleared legacy id refresh token because refresh is clearing other tokens" - ); - cookieAndHeaders_1.setCookie( - config, - res, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "accessTokenPath" - ); - } - } - throw err; - } - }); - }, - regenerateAccessToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let newAccessTokenPayload = - input.newAccessTokenPayload === null || input.newAccessTokenPayload === undefined - ? {} - : input.newAccessTokenPayload; - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/regenerate"), - { - accessToken: input.accessToken, - userDataInJWT: newAccessTokenPayload, - } - ); - if (response.status === "UNAUTHORISED") { - return undefined; - } - return response; - }); - }, - revokeAllSessionsForUser: function ({ userId }) { - return SessionFunctions.revokeAllSessionsForUser(helpers, userId); - }, - getAllSessionHandlesForUser: function ({ userId }) { - return SessionFunctions.getAllSessionHandlesForUser(helpers, userId); - }, - revokeSession: function ({ sessionHandle }) { - return SessionFunctions.revokeSession(helpers, sessionHandle); - }, - revokeMultipleSessions: function ({ sessionHandles }) { - return SessionFunctions.revokeMultipleSessions(helpers, sessionHandles); - }, - updateSessionData: function ({ sessionHandle, newSessionData }) { - return SessionFunctions.updateSessionData(helpers, sessionHandle, newSessionData); - }, - updateAccessTokenPayload: function ({ sessionHandle, newAccessTokenPayload }) { - return SessionFunctions.updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload); - }, - mergeIntoAccessTokenPayload: function ({ sessionHandle, accessTokenPayloadUpdate, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - const sessionInfo = yield this.getSessionInformation({ sessionHandle, userContext }); - if (sessionInfo === undefined) { - return false; - } - const newAccessTokenPayload = Object.assign( - Object.assign({}, sessionInfo.accessTokenPayload), - accessTokenPayloadUpdate - ); - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete newAccessTokenPayload[key]; - } - } - return this.updateAccessTokenPayload({ sessionHandle, newAccessTokenPayload, userContext }); - }); - }, - getAccessTokenLifeTimeMS: function () { - return __awaiter(this, void 0, void 0, function* () { - return (yield getHandshakeInfo()).accessTokenValidity; - }); - }, - getRefreshTokenLifeTimeMS: function () { - return __awaiter(this, void 0, void 0, function* () { - return (yield getHandshakeInfo()).refreshTokenValidity; - }); - }, - fetchAndSetClaim: function (input) { - return __awaiter(this, void 0, void 0, function* () { - const sessionInfo = yield this.getSessionInformation({ - sessionHandle: input.sessionHandle, - userContext: input.userContext, - }); - if (sessionInfo === undefined) { - return false; - } - const accessTokenPayloadUpdate = yield input.claim.build(sessionInfo.userId, input.userContext); - return this.mergeIntoAccessTokenPayload({ - sessionHandle: input.sessionHandle, - accessTokenPayloadUpdate, - userContext: input.userContext, - }); - }); - }, - setClaimValue: function (input) { - const accessTokenPayloadUpdate = input.claim.addToPayload_internal({}, input.value, input.userContext); - return this.mergeIntoAccessTokenPayload({ - sessionHandle: input.sessionHandle, - accessTokenPayloadUpdate, - userContext: input.userContext, - }); - }, - getClaimValue: function (input) { - return __awaiter(this, void 0, void 0, function* () { - const sessionInfo = yield this.getSessionInformation({ - sessionHandle: input.sessionHandle, - userContext: input.userContext, - }); - if (sessionInfo === undefined) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - return { - status: "OK", - value: input.claim.getValueFromPayload(sessionInfo.accessTokenPayload, input.userContext), - }; - }); - }, - removeClaim: function (input) { - const accessTokenPayloadUpdate = input.claim.removeFromPayloadByMerge_internal({}, input.userContext); - return this.mergeIntoAccessTokenPayload({ - sessionHandle: input.sessionHandle, - accessTokenPayloadUpdate, - userContext: input.userContext, - }); - }, - }; - let helpers = { - querier, - updateJwtSigningPublicKeyInfo, - getHandshakeInfo, - config, - appInfo, - getRecipeImpl: getRecipeImplAfterOverrides, - }; - if (process.env.TEST_MODE === "testing") { - // testing mode, we add some of the help functions to the obj - obj.getHandshakeInfo = getHandshakeInfo; - obj.updateJwtSigningPublicKeyInfo = updateJwtSigningPublicKeyInfo; - obj.helpers = helpers; - obj.setHandshakeInfo = function (info) { - handshakeInfo = info; - }; - } - return obj; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/session/sessionClass.d.ts b/lib/build/recipe/session/sessionClass.d.ts deleted file mode 100644 index 4a7f6d0a3..000000000 --- a/lib/build/recipe/session/sessionClass.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import { SessionClaim, SessionClaimValidator, SessionContainerInterface, TokenTransferMethod } from "./types"; -import { Helpers } from "./recipeImplementation"; -export default class Session implements SessionContainerInterface { - protected helpers: Helpers; - protected accessToken: string; - protected sessionHandle: string; - protected userId: string; - protected userDataInAccessToken: any; - protected res: BaseResponse; - protected readonly req: BaseRequest; - protected readonly transferMethod: TokenTransferMethod; - constructor( - helpers: Helpers, - accessToken: string, - sessionHandle: string, - userId: string, - userDataInAccessToken: any, - res: BaseResponse, - req: BaseRequest, - transferMethod: TokenTransferMethod - ); - revokeSession(userContext?: any): Promise; - getSessionData(userContext?: any): Promise; - updateSessionData(newSessionData: any, userContext?: any): Promise; - getUserId(_userContext?: any): string; - getAccessTokenPayload(_userContext?: any): any; - getHandle(): string; - getAccessToken(): string; - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: any, userContext?: any): Promise; - getTimeCreated(userContext?: any): Promise; - getExpiry(userContext?: any): Promise; - assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise; - fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise; - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise; - getClaimValue(claim: SessionClaim, userContext?: any): Promise; - removeClaim(claim: SessionClaim, userContext?: any): Promise; - /** - * @deprecated Use mergeIntoAccessTokenPayload - */ - updateAccessTokenPayload(newAccessTokenPayload: any, userContext: any): Promise; -} diff --git a/lib/build/recipe/session/sessionClass.js b/lib/build/recipe/session/sessionClass.js deleted file mode 100644 index db6d1bd1a..000000000 --- a/lib/build/recipe/session/sessionClass.js +++ /dev/null @@ -1,240 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const cookieAndHeaders_1 = require("./cookieAndHeaders"); -const error_1 = __importDefault(require("./error")); -class Session { - constructor(helpers, accessToken, sessionHandle, userId, userDataInAccessToken, res, req, transferMethod) { - this.helpers = helpers; - this.accessToken = accessToken; - this.sessionHandle = sessionHandle; - this.userId = userId; - this.userDataInAccessToken = userDataInAccessToken; - this.res = res; - this.req = req; - this.transferMethod = transferMethod; - } - revokeSession(userContext) { - return __awaiter(this, void 0, void 0, function* () { - yield this.helpers.getRecipeImpl().revokeSession({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - // we do not check the output of calling revokeSession - // before clearing the cookies because we are revoking the - // current API request's session. - // If we instead clear the cookies only when revokeSession - // returns true, it can cause this kind of a bug: - // https://github.com/supertokens/supertokens-node/issues/343 - cookieAndHeaders_1.clearSession(this.helpers.config, this.res, this.transferMethod); - }); - } - getSessionData(userContext) { - return __awaiter(this, void 0, void 0, function* () { - let sessionInfo = yield this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - if (sessionInfo === undefined) { - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - return sessionInfo.sessionData; - }); - } - updateSessionData(newSessionData, userContext) { - return __awaiter(this, void 0, void 0, function* () { - if ( - !(yield this.helpers.getRecipeImpl().updateSessionData({ - sessionHandle: this.sessionHandle, - newSessionData, - userContext: userContext === undefined ? {} : userContext, - })) - ) { - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - }); - } - getUserId(_userContext) { - return this.userId; - } - getAccessTokenPayload(_userContext) { - return this.userDataInAccessToken; - } - getHandle() { - return this.sessionHandle; - } - getAccessToken() { - return this.accessToken; - } - // Any update to this function should also be reflected in the respective JWT version - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const updatedPayload = Object.assign( - Object.assign({}, this.getAccessTokenPayload(userContext)), - accessTokenPayloadUpdate - ); - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete updatedPayload[key]; - } - } - yield this.updateAccessTokenPayload(updatedPayload, userContext); - }); - } - getTimeCreated(userContext) { - return __awaiter(this, void 0, void 0, function* () { - let sessionInfo = yield this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - if (sessionInfo === undefined) { - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - return sessionInfo.timeCreated; - }); - } - getExpiry(userContext) { - return __awaiter(this, void 0, void 0, function* () { - let sessionInfo = yield this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - if (sessionInfo === undefined) { - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - return sessionInfo.expiry; - }); - } - // Any update to this function should also be reflected in the respective JWT version - assertClaims(claimValidators, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let validateClaimResponse = yield this.helpers.getRecipeImpl().validateClaims({ - accessTokenPayload: this.getAccessTokenPayload(userContext), - userId: this.getUserId(userContext), - claimValidators, - userContext, - }); - if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { - yield this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); - } - if (validateClaimResponse.invalidClaims.length !== 0) { - throw new error_1.default({ - type: "INVALID_CLAIMS", - message: "INVALID_CLAIMS", - payload: validateClaimResponse.invalidClaims, - }); - } - }); - } - // Any update to this function should also be reflected in the respective JWT version - fetchAndSetClaim(claim, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const update = yield claim.build(this.getUserId(userContext), userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - }); - } - // Any update to this function should also be reflected in the respective JWT version - setClaimValue(claim, value, userContext) { - const update = claim.addToPayload_internal({}, value, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - // Any update to this function should also be reflected in the respective JWT version - getClaimValue(claim, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return claim.getValueFromPayload(yield this.getAccessTokenPayload(userContext), userContext); - }); - } - // Any update to this function should also be reflected in the respective JWT version - removeClaim(claim, userContext) { - const update = claim.removeFromPayloadByMerge_internal({}, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - /** - * @deprecated Use mergeIntoAccessTokenPayload - */ - updateAccessTokenPayload(newAccessTokenPayload, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield this.helpers.getRecipeImpl().regenerateAccessToken({ - accessToken: this.getAccessToken(), - newAccessTokenPayload, - userContext: userContext === undefined ? {} : userContext, - }); - if (response === undefined) { - throw new error_1.default({ - message: "Session does not exist anymore", - type: error_1.default.UNAUTHORISED, - }); - } - this.userDataInAccessToken = response.session.userDataInJWT; - if (response.accessToken !== undefined) { - this.accessToken = response.accessToken.token; - cookieAndHeaders_1.setFrontTokenInHeaders( - this.res, - response.session.userId, - response.accessToken.expiry, - response.session.userDataInJWT - ); - cookieAndHeaders_1.setToken( - this.helpers.config, - this.res, - "access", - response.accessToken.token, - // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. - // Even if the token is expired the presence of the token indicates that the user could have a valid refresh - // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. - Date.now() + 3153600000000, - this.transferMethod - ); - } - }); - } -} -exports.default = Session; diff --git a/lib/build/recipe/session/sessionFunctions.d.ts b/lib/build/recipe/session/sessionFunctions.d.ts deleted file mode 100644 index 909826b5b..000000000 --- a/lib/build/recipe/session/sessionFunctions.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -// @ts-nocheck -import { ParsedJWTInfo } from "./jwt"; -import { CreateOrRefreshAPIResponse, SessionInformation, TokenTransferMethod } from "./types"; -import { Helpers } from "./recipeImplementation"; -/** - * @description call this to "login" a user. - */ -export declare function createNewSession( - helpers: Helpers, - userId: string, - disableAntiCsrf: boolean, - accessTokenPayload?: any, - sessionData?: any -): Promise; -/** - * @description authenticates a session. To be used in APIs that require authentication - */ -export declare function getSession( - helpers: Helpers, - parsedAccessToken: ParsedJWTInfo, - antiCsrfToken: string | undefined, - doAntiCsrfCheck: boolean, - containsCustomHeader: boolean -): Promise<{ - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken?: { - token: string; - expiry: number; - createdTime: number; - }; -}>; -/** - * @description Retrieves session information from storage for a given session handle - * @returns session data stored in the database, including userData and access token payload, or undefined if sessionHandle is invalid - */ -export declare function getSessionInformation( - helpers: Helpers, - sessionHandle: string -): Promise; -/** - * @description generates new access and refresh tokens for a given refresh token. Called when client's access token has expired. - * @sideEffects calls onTokenTheftDetection if token theft is detected. - */ -export declare function refreshSession( - helpers: Helpers, - refreshToken: string, - antiCsrfToken: string | undefined, - containsCustomHeader: boolean, - transferMethod: TokenTransferMethod -): Promise; -/** - * @description deletes session info of a user from db. This only invalidates the refresh token. Not the access token. - * Access tokens cannot be immediately invalidated. Unless we add a blacklisting method. Or changed the private key to sign them. - */ -export declare function revokeAllSessionsForUser(helpers: Helpers, userId: string): Promise; -/** - * @description gets all session handles for current user. Please do not call this unless this user is authenticated. - */ -export declare function getAllSessionHandlesForUser(helpers: Helpers, userId: string): Promise; -/** - * @description call to destroy one session - * @returns true if session was deleted from db. Else false in case there was nothing to delete - */ -export declare function revokeSession(helpers: Helpers, sessionHandle: string): Promise; -/** - * @description call to destroy multiple sessions - * @returns list of sessions revoked - */ -export declare function revokeMultipleSessions(helpers: Helpers, sessionHandles: string[]): Promise; -/** - * @description: It provides no locking mechanism in case other processes are updating session data for this session as well. - */ -export declare function updateSessionData( - helpers: Helpers, - sessionHandle: string, - newSessionData: any -): Promise; -export declare function updateAccessTokenPayload( - helpers: Helpers, - sessionHandle: string, - newAccessTokenPayload: any -): Promise; diff --git a/lib/build/recipe/session/sessionFunctions.js b/lib/build/recipe/session/sessionFunctions.js deleted file mode 100644 index 072ea7429..000000000 --- a/lib/build/recipe/session/sessionFunctions.js +++ /dev/null @@ -1,440 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.updateAccessTokenPayload = exports.updateSessionData = exports.revokeMultipleSessions = exports.revokeSession = exports.getAllSessionHandlesForUser = exports.revokeAllSessionsForUser = exports.refreshSession = exports.getSessionInformation = exports.getSession = exports.createNewSession = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const accessToken_1 = require("./accessToken"); -const error_1 = __importDefault(require("./error")); -const processState_1 = require("../../processState"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const utils_1 = require("../../utils"); -const logger_1 = require("../../logger"); -/** - * @description call this to "login" a user. - */ -function createNewSession(helpers, userId, disableAntiCsrf, accessTokenPayload = {}, sessionData = {}) { - return __awaiter(this, void 0, void 0, function* () { - accessTokenPayload = accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; - sessionData = sessionData === null || sessionData === undefined ? {} : sessionData; - let requestBody = { - userId, - userDataInJWT: accessTokenPayload, - userDataInDatabase: sessionData, - }; - let handShakeInfo = yield helpers.getHandshakeInfo(); - requestBody.enableAntiCsrf = !disableAntiCsrf && handShakeInfo.antiCsrf === "VIA_TOKEN"; - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session"), - requestBody - ); - helpers.updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - delete response.status; - delete response.jwtSigningPublicKey; - delete response.jwtSigningPublicKeyExpiryTime; - delete response.jwtSigningPublicKeyList; - return response; - }); -} -exports.createNewSession = createNewSession; -/** - * @description authenticates a session. To be used in APIs that require authentication - */ -function getSession(helpers, parsedAccessToken, antiCsrfToken, doAntiCsrfCheck, containsCustomHeader) { - return __awaiter(this, void 0, void 0, function* () { - let handShakeInfo = yield helpers.getHandshakeInfo(); - let accessTokenInfo; - // If we have no key old enough to verify this access token we should reject it without calling the core - let foundASigningKeyThatIsOlderThanTheAccessToken = false; - for (const key of handShakeInfo.getJwtSigningPublicKeyList()) { - try { - /** - * get access token info using existing signingKey - */ - accessTokenInfo = yield accessToken_1.getInfoFromAccessToken( - parsedAccessToken, - key.publicKey, - handShakeInfo.antiCsrf === "VIA_TOKEN" && doAntiCsrfCheck - ); - foundASigningKeyThatIsOlderThanTheAccessToken = true; - } catch (err) { - /** - * if error type is not TRY_REFRESH_TOKEN, we return the - * error to the user - */ - if (err.type !== error_1.default.TRY_REFRESH_TOKEN) { - throw err; - } - /** - * if it comes here, it means token verification has failed. - * It may be due to: - * - signing key was updated and this token was signed with new key - * - access token is actually expired - * - access token was signed with the older signing key - * - * if access token is actually expired, we don't need to call core and - * just return TRY_REFRESH_TOKEN to the client - * - * if access token creation time is after this signing key was created - * we need to call core as there are chances that the token - * was signed with the updated signing key - * - * if access token creation time is before oldest signing key was created, - * so if foundASigningKeyThatIsOlderThanTheAccessToken is still false after - * the loop we just return TRY_REFRESH_TOKEN - */ - let payload = parsedAccessToken.payload; - const timeCreated = accessToken_1.sanitizeNumberInput(payload.timeCreated); - const expiryTime = accessToken_1.sanitizeNumberInput(payload.expiryTime); - if (expiryTime === undefined || expiryTime < Date.now()) { - throw err; - } - if (timeCreated === undefined) { - throw err; - } - // If we reached a key older than the token and failed to validate the token, - // that means it was signed by a key newer than the cached list. - // In this case we go to the server. - if (timeCreated >= key.createdAt) { - foundASigningKeyThatIsOlderThanTheAccessToken = true; - break; - } - } - } - // If the token was created before the oldest key in the cache but hasn't expired, then a config value must've changed. - // E.g., the access_token_signing_key_update_interval was reduced, or access_token_signing_key_dynamic was turned on. - // Either way, the user needs to refresh the access token as validating by the server is likely to do nothing. - if (!foundASigningKeyThatIsOlderThanTheAccessToken) { - throw new error_1.default({ - message: "Access token has expired. Please call the refresh API", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - /** - * anti-csrf check if accesstokenInfo is not undefined, - * which means token verification was successful - */ - if (doAntiCsrfCheck) { - if (handShakeInfo.antiCsrf === "VIA_TOKEN") { - if (accessTokenInfo !== undefined) { - if (antiCsrfToken === undefined || antiCsrfToken !== accessTokenInfo.antiCsrfToken) { - if (antiCsrfToken === undefined) { - logger_1.logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" - ); - throw new error_1.default({ - message: - "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } else { - logger_1.logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" - ); - throw new error_1.default({ - message: "anti-csrf check failed", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - } - } - } else if (handShakeInfo.antiCsrf === "VIA_CUSTOM_HEADER") { - if (!containsCustomHeader) { - logger_1.logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed" - ); - throw new error_1.default({ - message: - "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request, or set doAntiCsrfCheck to false for this API", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - } - } - if ( - accessTokenInfo !== undefined && - !handShakeInfo.accessTokenBlacklistingEnabled && - accessTokenInfo.parentRefreshTokenHash1 === undefined - ) { - return { - session: { - handle: accessTokenInfo.sessionHandle, - userId: accessTokenInfo.userId, - userDataInJWT: accessTokenInfo.userData, - }, - }; - } - processState_1.ProcessState.getInstance().addState(processState_1.PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - let requestBody = { - accessToken: parsedAccessToken.rawTokenString, - antiCsrfToken, - doAntiCsrfCheck, - enableAntiCsrf: handShakeInfo.antiCsrf === "VIA_TOKEN", - }; - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/verify"), - requestBody - ); - if (response.status === "OK") { - helpers.updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - delete response.status; - delete response.jwtSigningPublicKey; - delete response.jwtSigningPublicKeyExpiryTime; - delete response.jwtSigningPublicKeyList; - return response; - } else if (response.status === "UNAUTHORISED") { - logger_1.logDebugMessage("getSession: Returning UNAUTHORISED because of core response"); - throw new error_1.default({ - message: response.message, - type: error_1.default.UNAUTHORISED, - }); - } else { - if ( - response.jwtSigningPublicKeyList !== undefined || - (response.jwtSigningPublicKey !== undefined && response.jwtSigningPublicKeyExpiryTime !== undefined) - ) { - // after CDI 2.7.1, the API returns the new keys - helpers.updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - } else { - // we force update the signing keys... - yield helpers.getHandshakeInfo(true); - } - logger_1.logDebugMessage("getSession: Returning TRY_REFRESH_TOKEN because of core response."); - throw new error_1.default({ - message: response.message, - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - }); -} -exports.getSession = getSession; -/** - * @description Retrieves session information from storage for a given session handle - * @returns session data stored in the database, including userData and access token payload, or undefined if sessionHandle is invalid - */ -function getSessionInformation(helpers, sessionHandle) { - return __awaiter(this, void 0, void 0, function* () { - let apiVersion = yield helpers.querier.getAPIVersion(); - if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error("Please use core version >= 3.5 to call this function."); - } - let response = yield helpers.querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/session"), { - sessionHandle, - }); - if (response.status === "OK") { - // Change keys to make them more readable - response["sessionData"] = response.userDataInDatabase; - response["accessTokenPayload"] = response.userDataInJWT; - delete response.userDataInJWT; - delete response.userDataInJWT; - return response; - } else { - return undefined; - } - }); -} -exports.getSessionInformation = getSessionInformation; -/** - * @description generates new access and refresh tokens for a given refresh token. Called when client's access token has expired. - * @sideEffects calls onTokenTheftDetection if token theft is detected. - */ -function refreshSession(helpers, refreshToken, antiCsrfToken, containsCustomHeader, transferMethod) { - return __awaiter(this, void 0, void 0, function* () { - let handShakeInfo = yield helpers.getHandshakeInfo(); - let requestBody = { - refreshToken, - antiCsrfToken, - enableAntiCsrf: transferMethod === "cookie" && handShakeInfo.antiCsrf === "VIA_TOKEN", - }; - if (handShakeInfo.antiCsrf === "VIA_CUSTOM_HEADER" && transferMethod === "cookie") { - if (!containsCustomHeader) { - logger_1.logDebugMessage( - "refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed" - ); - throw new error_1.default({ - message: "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request.", - type: error_1.default.UNAUTHORISED, - payload: { - clearTokens: false, // see https://github.com/supertokens/supertokens-node/issues/141 - }, - }); - } - } - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/refresh"), - requestBody - ); - if (response.status === "OK") { - delete response.status; - return response; - } else if (response.status === "UNAUTHORISED") { - logger_1.logDebugMessage("refreshSession: Returning UNAUTHORISED because of core response"); - throw new error_1.default({ - message: response.message, - type: error_1.default.UNAUTHORISED, - }); - } else { - logger_1.logDebugMessage("refreshSession: Returning TOKEN_THEFT_DETECTED because of core response"); - throw new error_1.default({ - message: "Token theft detected", - payload: { - userId: response.session.userId, - sessionHandle: response.session.handle, - }, - type: error_1.default.TOKEN_THEFT_DETECTED, - }); - } - }); -} -exports.refreshSession = refreshSession; -/** - * @description deletes session info of a user from db. This only invalidates the refresh token. Not the access token. - * Access tokens cannot be immediately invalidated. Unless we add a blacklisting method. Or changed the private key to sign them. - */ -function revokeAllSessionsForUser(helpers, userId) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/remove"), - { - userId, - } - ); - return response.sessionHandlesRevoked; - }); -} -exports.revokeAllSessionsForUser = revokeAllSessionsForUser; -/** - * @description gets all session handles for current user. Please do not call this unless this user is authenticated. - */ -function getAllSessionHandlesForUser(helpers, userId) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield helpers.querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/session/user"), { - userId, - }); - return response.sessionHandles; - }); -} -exports.getAllSessionHandlesForUser = getAllSessionHandlesForUser; -/** - * @description call to destroy one session - * @returns true if session was deleted from db. Else false in case there was nothing to delete - */ -function revokeSession(helpers, sessionHandle) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/remove"), - { - sessionHandles: [sessionHandle], - } - ); - return response.sessionHandlesRevoked.length === 1; - }); -} -exports.revokeSession = revokeSession; -/** - * @description call to destroy multiple sessions - * @returns list of sessions revoked - */ -function revokeMultipleSessions(helpers, sessionHandles) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield helpers.querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/session/remove"), - { - sessionHandles, - } - ); - return response.sessionHandlesRevoked; - }); -} -exports.revokeMultipleSessions = revokeMultipleSessions; -/** - * @description: It provides no locking mechanism in case other processes are updating session data for this session as well. - */ -function updateSessionData(helpers, sessionHandle, newSessionData) { - return __awaiter(this, void 0, void 0, function* () { - newSessionData = newSessionData === null || newSessionData === undefined ? {} : newSessionData; - let response = yield helpers.querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/session/data"), { - sessionHandle, - userDataInDatabase: newSessionData, - }); - if (response.status === "UNAUTHORISED") { - return false; - } - return true; - }); -} -exports.updateSessionData = updateSessionData; -function updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload) { - return __awaiter(this, void 0, void 0, function* () { - newAccessTokenPayload = - newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - let response = yield helpers.querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/jwt/data"), { - sessionHandle, - userDataInJWT: newAccessTokenPayload, - }); - if (response.status === "UNAUTHORISED") { - return false; - } - return true; - }); -} -exports.updateAccessTokenPayload = updateAccessTokenPayload; diff --git a/lib/build/recipe/session/types.d.ts b/lib/build/recipe/session/types.d.ts deleted file mode 100644 index 6165936b6..000000000 --- a/lib/build/recipe/session/types.d.ts +++ /dev/null @@ -1,424 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; -import OverrideableBuilder from "supertokens-js-override"; -import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; -import { JSONObject, JSONValue } from "../../types"; -import { GeneralErrorResponse } from "../../types"; -export declare type KeyInfo = { - publicKey: string; - expiryTime: number; - createdAt: number; -}; -export declare type AntiCsrfType = "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE"; -export declare type StoredHandshakeInfo = { - antiCsrf: AntiCsrfType; - accessTokenBlacklistingEnabled: boolean; - accessTokenValidity: number; - refreshTokenValidity: number; -} & ( - | { - jwtSigningPublicKeyList: KeyInfo[]; - } - | { - jwtSigningPublicKeyList: undefined; - jwtSigningPublicKey: string; - jwtSigningPublicKeyExpiryTime: number; - } -); -export declare type CreateOrRefreshAPIResponse = { - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken: { - token: string; - expiry: number; - createdTime: number; - }; - refreshToken: { - token: string; - expiry: number; - createdTime: number; - }; - antiCsrfToken: string | undefined; -}; -export interface ErrorHandlers { - onUnauthorised?: ErrorHandlerMiddleware; - onTokenTheftDetected?: TokenTheftErrorHandlerMiddleware; - onInvalidClaim?: InvalidClaimErrorHandlerMiddleware; -} -export declare type TokenType = "access" | "refresh"; -export declare type TokenTransferMethod = "header" | "cookie"; -export declare type TypeInput = { - sessionExpiredStatusCode?: number; - invalidClaimStatusCode?: number; - accessTokenPath?: string; - cookieSecure?: boolean; - cookieSameSite?: "strict" | "lax" | "none"; - cookieDomain?: string; - getTokenTransferMethod?: (input: { - req: BaseRequest; - forCreateNewSession: boolean; - userContext: any; - }) => TokenTransferMethod | "any"; - errorHandlers?: ErrorHandlers; - antiCsrf?: "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE"; - jwt?: - | { - enable: true; - propertyNameInAccessTokenPayload?: string; - issuer?: string; - } - | { - enable: false; - }; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; - }; -}; -export declare type TypeNormalisedInput = { - refreshTokenPath: NormalisedURLPath; - accessTokenPath: NormalisedURLPath; - cookieDomain: string | undefined; - cookieSameSite: "strict" | "lax" | "none"; - cookieSecure: boolean; - sessionExpiredStatusCode: number; - errorHandlers: NormalisedErrorHandlers; - antiCsrf: "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE"; - getTokenTransferMethod: (input: { - req: BaseRequest; - forCreateNewSession: boolean; - userContext: any; - }) => TokenTransferMethod | "any"; - invalidClaimStatusCode: number; - jwt: { - enable: boolean; - propertyNameInAccessTokenPayload: string; - issuer?: string; - }; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; - }; -}; -export interface SessionRequest extends BaseRequest { - session?: SessionContainerInterface; -} -export interface ErrorHandlerMiddleware { - (message: string, request: BaseRequest, response: BaseResponse): Promise; -} -export interface TokenTheftErrorHandlerMiddleware { - (sessionHandle: string, userId: string, request: BaseRequest, response: BaseResponse): Promise; -} -export interface InvalidClaimErrorHandlerMiddleware { - (validatorErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse): Promise; -} -export interface NormalisedErrorHandlers { - onUnauthorised: ErrorHandlerMiddleware; - onTryRefreshToken: ErrorHandlerMiddleware; - onTokenTheftDetected: TokenTheftErrorHandlerMiddleware; - onInvalidClaim: InvalidClaimErrorHandlerMiddleware; -} -export interface VerifySessionOptions { - antiCsrfCheck?: boolean; - sessionRequired?: boolean; - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - session: SessionContainerInterface, - userContext: any - ) => Promise | SessionClaimValidator[]; -} -export declare type RecipeInterface = { - createNewSession(input: { - req: BaseRequest; - res: BaseResponse; - userId: string; - accessTokenPayload?: any; - sessionData?: any; - userContext: any; - }): Promise; - getGlobalClaimValidators(input: { - userId: string; - claimValidatorsAddedByOtherRecipes: SessionClaimValidator[]; - userContext: any; - }): Promise | SessionClaimValidator[]; - getSession(input: { - req: BaseRequest; - res: BaseResponse; - options?: VerifySessionOptions; - userContext: any; - }): Promise; - refreshSession(input: { - req: BaseRequest; - res: BaseResponse; - userContext: any; - }): Promise; - /** - * Used to retrieve all session information for a given session handle. Can be used in place of: - * - getSessionData - * - getAccessTokenPayload - * - * Returns undefined if the sessionHandle does not exist - */ - getSessionInformation(input: { sessionHandle: string; userContext: any }): Promise; - revokeAllSessionsForUser(input: { userId: string; userContext: any }): Promise; - getAllSessionHandlesForUser(input: { userId: string; userContext: any }): Promise; - revokeSession(input: { sessionHandle: string; userContext: any }): Promise; - revokeMultipleSessions(input: { sessionHandles: string[]; userContext: any }): Promise; - updateSessionData(input: { sessionHandle: string; newSessionData: any; userContext: any }): Promise; - /** - * @deprecated Use mergeIntoAccessTokenPayload instead - * @returns {Promise} Returns false if the sessionHandle does not exist - */ - updateAccessTokenPayload(input: { - sessionHandle: string; - newAccessTokenPayload: any; - userContext: any; - }): Promise; - mergeIntoAccessTokenPayload(input: { - sessionHandle: string; - accessTokenPayloadUpdate: JSONObject; - userContext: any; - }): Promise; - /** - * @returns {Promise} Returns false if the sessionHandle does not exist - */ - regenerateAccessToken(input: { - accessToken: string; - newAccessTokenPayload?: any; - userContext: any; - }): Promise< - | { - status: "OK"; - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken?: { - token: string; - expiry: number; - createdTime: number; - }; - } - | undefined - >; - getAccessTokenLifeTimeMS(input: { userContext: any }): Promise; - getRefreshTokenLifeTimeMS(input: { userContext: any }): Promise; - validateClaims(input: { - userId: string; - accessTokenPayload: any; - claimValidators: SessionClaimValidator[]; - userContext: any; - }): Promise<{ - invalidClaims: ClaimValidationError[]; - accessTokenPayloadUpdate?: any; - }>; - validateClaimsInJWTPayload(input: { - userId: string; - jwtPayload: JSONObject; - claimValidators: SessionClaimValidator[]; - userContext: any; - }): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }>; - fetchAndSetClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; - setClaimValue(input: { - sessionHandle: string; - claim: SessionClaim; - value: T; - userContext: any; - }): Promise; - getClaimValue(input: { - sessionHandle: string; - claim: SessionClaim; - userContext: any; - }): Promise< - | { - status: "SESSION_DOES_NOT_EXIST_ERROR"; - } - | { - status: "OK"; - value: T | undefined; - } - >; - removeClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; -}; -export interface SessionContainerInterface { - revokeSession(userContext?: any): Promise; - getSessionData(userContext?: any): Promise; - updateSessionData(newSessionData: any, userContext?: any): Promise; - getUserId(userContext?: any): string; - getAccessTokenPayload(userContext?: any): any; - getHandle(userContext?: any): string; - getAccessToken(userContext?: any): string; - /** - * @deprecated Use mergeIntoAccessTokenPayload instead - */ - updateAccessTokenPayload(newAccessTokenPayload: any, userContext?: any): Promise; - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: any): Promise; - getTimeCreated(userContext?: any): Promise; - getExpiry(userContext?: any): Promise; - assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise; - fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise; - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise; - getClaimValue(claim: SessionClaim, userContext?: any): Promise; - removeClaim(claim: SessionClaim, userContext?: any): Promise; -} -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; -}; -export declare type APIInterface = { - /** - * We do not add a GeneralErrorResponse response to this API - * since it's not something that is directly called by the user on the - * frontend anyway - */ - refreshPOST: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); - signOutPOST: - | undefined - | ((input: { - options: APIOptions; - session: SessionContainerInterface | undefined; - userContext: any; - }) => Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - >); - verifySession(input: { - verifySessionOptions: VerifySessionOptions | undefined; - options: APIOptions; - userContext: any; - }): Promise; -}; -export declare type SessionInformation = { - sessionHandle: string; - userId: string; - sessionData: any; - expiry: number; - accessTokenPayload: any; - timeCreated: number; -}; -export declare type ClaimValidationResult = - | { - isValid: true; - } - | { - isValid: false; - reason?: JSONValue; - }; -export declare type ClaimValidationError = { - id: string; - reason?: JSONValue; -}; -export declare type SessionClaimValidator = ( - | // We split the type like this to express that either both claim and shouldRefetch is defined or neither. - { - claim: SessionClaim; - /** - * Decides if we need to refetch the claim value before checking the payload with `isValid`. - * E.g.: if the information in the payload is expired, or is not sufficient for this check. - */ - shouldRefetch: (payload: any, userContext: any) => Promise | boolean; - } - | {} -) & { - id: string; - /** - * Decides if the claim is valid based on the payload (and not checking DB or anything else) - */ - validate: (payload: any, userContext: any) => Promise; -}; -export declare abstract class SessionClaim { - readonly key: string; - constructor(key: string); - /** - * This methods fetches the current value of this claim for the user. - * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database - * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. - */ - abstract fetchValue(userId: string, userContext: any): Promise | T | undefined; - /** - * Saves the provided value into the payload, by cloning and updating the entire object. - * - * @returns The modified payload object - */ - abstract addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - /** - * Removes the claim from the payload by setting it to null, so mergeIntoAccessTokenPayload clears it - * - * @returns The modified payload object - */ - abstract removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; - /** - * Removes the claim from the payload, by cloning and updating the entire object. - * - * @returns The modified payload object - */ - abstract removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; - /** - * Gets the value of the claim stored in the payload - * - * @returns Claim value - */ - abstract getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; - build(userId: string, userContext?: any): Promise; -} diff --git a/lib/build/recipe/session/types.js b/lib/build/recipe/session/types.js deleted file mode 100644 index af3c78734..000000000 --- a/lib/build/recipe/session/types.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SessionClaim = void 0; -class SessionClaim { - constructor(key) { - this.key = key; - } - build(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const value = yield this.fetchValue(userId, userContext); - if (value === undefined) { - return {}; - } - return this.addToPayload_internal({}, value, userContext); - }); - } -} -exports.SessionClaim = SessionClaim; diff --git a/lib/build/recipe/session/utils.d.ts b/lib/build/recipe/session/utils.d.ts deleted file mode 100644 index 29997b4a7..000000000 --- a/lib/build/recipe/session/utils.d.ts +++ /dev/null @@ -1,68 +0,0 @@ -// @ts-nocheck -import { - CreateOrRefreshAPIResponse, - TypeInput, - TypeNormalisedInput, - ClaimValidationError, - SessionClaimValidator, - SessionContainerInterface, - VerifySessionOptions, - TokenTransferMethod, -} from "./types"; -import SessionRecipe from "./recipe"; -import { NormalisedAppinfo } from "../../types"; -import { BaseRequest, BaseResponse } from "../../framework"; -export declare function sendTryRefreshTokenResponse( - recipeInstance: SessionRecipe, - _: string, - __: BaseRequest, - response: BaseResponse -): Promise; -export declare function sendUnauthorisedResponse( - recipeInstance: SessionRecipe, - _: string, - __: BaseRequest, - response: BaseResponse -): Promise; -export declare function sendInvalidClaimResponse( - recipeInstance: SessionRecipe, - claimValidationErrors: ClaimValidationError[], - __: BaseRequest, - response: BaseResponse -): Promise; -export declare function sendTokenTheftDetectedResponse( - recipeInstance: SessionRecipe, - sessionHandle: string, - _: string, - __: BaseRequest, - response: BaseResponse -): Promise; -export declare function normaliseSessionScopeOrThrowError(sessionScope: string): string; -export declare function getURLProtocol(url: string): string; -export declare function validateAndNormaliseUserInput( - recipeInstance: SessionRecipe, - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; -export declare function normaliseSameSiteOrThrowError(sameSite: string): "strict" | "lax" | "none"; -export declare function attachTokensToResponse( - config: TypeNormalisedInput, - res: BaseResponse, - response: CreateOrRefreshAPIResponse, - transferMethod: TokenTransferMethod -): void; -export declare function getRequiredClaimValidators( - session: SessionContainerInterface, - overrideGlobalClaimValidators: VerifySessionOptions["overrideGlobalClaimValidators"], - userContext: any -): Promise; -export declare function validateClaimsInPayload( - claimValidators: SessionClaimValidator[], - newAccessTokenPayload: any, - userContext: any -): Promise< - { - id: string; - reason: import("../../types").JSONValue; - }[] ->; diff --git a/lib/build/recipe/session/utils.js b/lib/build/recipe/session/utils.js deleted file mode 100644 index 33d14782f..000000000 --- a/lib/build/recipe/session/utils.js +++ /dev/null @@ -1,339 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateClaimsInPayload = exports.getRequiredClaimValidators = exports.attachTokensToResponse = exports.normaliseSameSiteOrThrowError = exports.validateAndNormaliseUserInput = exports.getURLProtocol = exports.normaliseSessionScopeOrThrowError = exports.sendTokenTheftDetectedResponse = exports.sendInvalidClaimResponse = exports.sendUnauthorisedResponse = exports.sendTryRefreshTokenResponse = void 0; -const cookieAndHeaders_1 = require("./cookieAndHeaders"); -const url_1 = require("url"); -const recipe_1 = __importDefault(require("./recipe")); -const constants_1 = require("./constants"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const utils_1 = require("../../utils"); -const utils_2 = require("../../utils"); -const constants_2 = require("./with-jwt/constants"); -const logger_1 = require("../../logger"); -function sendTryRefreshTokenResponse(recipeInstance, _, __, response) { - return __awaiter(this, void 0, void 0, function* () { - utils_2.sendNon200ResponseWithMessage( - response, - "try refresh token", - recipeInstance.config.sessionExpiredStatusCode - ); - }); -} -exports.sendTryRefreshTokenResponse = sendTryRefreshTokenResponse; -function sendUnauthorisedResponse(recipeInstance, _, __, response) { - return __awaiter(this, void 0, void 0, function* () { - utils_2.sendNon200ResponseWithMessage(response, "unauthorised", recipeInstance.config.sessionExpiredStatusCode); - }); -} -exports.sendUnauthorisedResponse = sendUnauthorisedResponse; -function sendInvalidClaimResponse(recipeInstance, claimValidationErrors, __, response) { - return __awaiter(this, void 0, void 0, function* () { - utils_2.sendNon200Response(response, recipeInstance.config.invalidClaimStatusCode, { - message: "invalid claim", - claimValidationErrors, - }); - }); -} -exports.sendInvalidClaimResponse = sendInvalidClaimResponse; -function sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, _, __, response) { - return __awaiter(this, void 0, void 0, function* () { - yield recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }); - utils_2.sendNon200ResponseWithMessage( - response, - "token theft detected", - recipeInstance.config.sessionExpiredStatusCode - ); - }); -} -exports.sendTokenTheftDetectedResponse = sendTokenTheftDetectedResponse; -function normaliseSessionScopeOrThrowError(sessionScope) { - function helper(sessionScope) { - sessionScope = sessionScope.trim().toLowerCase(); - // first we convert it to a URL so that we can use the URL class - if (sessionScope.startsWith(".")) { - sessionScope = sessionScope.substr(1); - } - if (!sessionScope.startsWith("http://") && !sessionScope.startsWith("https://")) { - sessionScope = "http://" + sessionScope; - } - try { - let urlObj = new url_1.URL(sessionScope); - sessionScope = urlObj.hostname; - // remove leading dot - if (sessionScope.startsWith(".")) { - sessionScope = sessionScope.substr(1); - } - return sessionScope; - } catch (err) { - throw new Error("Please provide a valid sessionScope"); - } - } - let noDotNormalised = helper(sessionScope); - if (noDotNormalised === "localhost" || utils_1.isAnIpAddress(noDotNormalised)) { - return noDotNormalised; - } - if (sessionScope.startsWith(".")) { - return "." + noDotNormalised; - } - return noDotNormalised; -} -exports.normaliseSessionScopeOrThrowError = normaliseSessionScopeOrThrowError; -function getURLProtocol(url) { - let urlObj = new url_1.URL(url); - return urlObj.protocol; -} -exports.getURLProtocol = getURLProtocol; -function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { - var _a; - let cookieDomain = - config === undefined || config.cookieDomain === undefined - ? undefined - : normaliseSessionScopeOrThrowError(config.cookieDomain); - let accessTokenPath = - config === undefined || config.accessTokenPath === undefined - ? new normalisedURLPath_1.default("/") - : new normalisedURLPath_1.default(config.accessTokenPath); - let protocolOfAPIDomain = getURLProtocol(appInfo.apiDomain.getAsStringDangerous()); - let protocolOfWebsiteDomain = getURLProtocol(appInfo.websiteDomain.getAsStringDangerous()); - let cookieSameSite = - appInfo.topLevelAPIDomain !== appInfo.topLevelWebsiteDomain || protocolOfAPIDomain !== protocolOfWebsiteDomain - ? "none" - : "lax"; - cookieSameSite = - config === undefined || config.cookieSameSite === undefined - ? cookieSameSite - : normaliseSameSiteOrThrowError(config.cookieSameSite); - let cookieSecure = - config === undefined || config.cookieSecure === undefined - ? appInfo.apiDomain.getAsStringDangerous().startsWith("https") - : config.cookieSecure; - let sessionExpiredStatusCode = - config === undefined || config.sessionExpiredStatusCode === undefined ? 401 : config.sessionExpiredStatusCode; - const invalidClaimStatusCode = - (_a = config === null || config === void 0 ? void 0 : config.invalidClaimStatusCode) !== null && _a !== void 0 - ? _a - : 403; - if (sessionExpiredStatusCode === invalidClaimStatusCode) { - throw new Error("sessionExpiredStatusCode and sessionExpiredStatusCode must be different"); - } - if (config !== undefined && config.antiCsrf !== undefined) { - if (config.antiCsrf !== "NONE" && config.antiCsrf !== "VIA_CUSTOM_HEADER" && config.antiCsrf !== "VIA_TOKEN") { - throw new Error("antiCsrf config must be one of 'NONE' or 'VIA_CUSTOM_HEADER' or 'VIA_TOKEN'"); - } - } - let antiCsrf = - config === undefined || config.antiCsrf === undefined - ? cookieSameSite === "none" - ? "VIA_CUSTOM_HEADER" - : "NONE" - : config.antiCsrf; - let errorHandlers = { - onTokenTheftDetected: (sessionHandle, userId, request, response) => - __awaiter(this, void 0, void 0, function* () { - return yield sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, userId, request, response); - }), - onTryRefreshToken: (message, request, response) => - __awaiter(this, void 0, void 0, function* () { - return yield sendTryRefreshTokenResponse(recipeInstance, message, request, response); - }), - onUnauthorised: (message, request, response) => - __awaiter(this, void 0, void 0, function* () { - return yield sendUnauthorisedResponse(recipeInstance, message, request, response); - }), - onInvalidClaim: (validationErrors, request, response) => { - return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response); - }, - }; - if (config !== undefined && config.errorHandlers !== undefined) { - if (config.errorHandlers.onTokenTheftDetected !== undefined) { - errorHandlers.onTokenTheftDetected = config.errorHandlers.onTokenTheftDetected; - } - if (config.errorHandlers.onUnauthorised !== undefined) { - errorHandlers.onUnauthorised = config.errorHandlers.onUnauthorised; - } - if (config.errorHandlers.onInvalidClaim !== undefined) { - errorHandlers.onInvalidClaim = config.errorHandlers.onInvalidClaim; - } - } - let enableJWT = false; - let accessTokenPayloadJWTPropertyName = "jwt"; - let issuer; - if (config !== undefined && config.jwt !== undefined && config.jwt.enable === true) { - enableJWT = true; - let jwtPropertyName = config.jwt.propertyNameInAccessTokenPayload; - issuer = config.jwt.issuer; - if (jwtPropertyName !== undefined) { - if (jwtPropertyName === constants_2.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY) { - throw new Error(constants_2.JWT_RESERVED_KEY_USE_ERROR_MESSAGE); - } - accessTokenPayloadJWTPropertyName = jwtPropertyName; - } - } - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - return { - refreshTokenPath: appInfo.apiBasePath.appendPath(new normalisedURLPath_1.default(constants_1.REFRESH_API_PATH)), - accessTokenPath, - getTokenTransferMethod: - (config === null || config === void 0 ? void 0 : config.getTokenTransferMethod) === undefined - ? defaultGetTokenTransferMethod - : config.getTokenTransferMethod, - cookieDomain, - cookieSameSite, - cookieSecure, - sessionExpiredStatusCode, - errorHandlers, - antiCsrf, - override, - invalidClaimStatusCode, - jwt: { - enable: enableJWT, - propertyNameInAccessTokenPayload: accessTokenPayloadJWTPropertyName, - issuer, - }, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; -function normaliseSameSiteOrThrowError(sameSite) { - sameSite = sameSite.trim(); - sameSite = sameSite.toLocaleLowerCase(); - if (sameSite !== "strict" && sameSite !== "lax" && sameSite !== "none") { - throw new Error(`cookie same site must be one of "strict", "lax", or "none"`); - } - return sameSite; -} -exports.normaliseSameSiteOrThrowError = normaliseSameSiteOrThrowError; -function attachTokensToResponse(config, res, response, transferMethod) { - let accessToken = response.accessToken; - let refreshToken = response.refreshToken; - cookieAndHeaders_1.setFrontTokenInHeaders( - res, - response.session.userId, - response.accessToken.expiry, - response.session.userDataInJWT - ); - cookieAndHeaders_1.setToken( - config, - res, - "access", - accessToken.token, - // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. - // Even if the token is expired the presence of the token indicates that the user could have a valid refresh - // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. - Date.now() + 3153600000000, - transferMethod - ); - cookieAndHeaders_1.setToken(config, res, "refresh", refreshToken.token, refreshToken.expiry, transferMethod); - if (response.antiCsrfToken !== undefined) { - cookieAndHeaders_1.setAntiCsrfTokenInHeaders(res, response.antiCsrfToken); - } -} -exports.attachTokensToResponse = attachTokensToResponse; -function getRequiredClaimValidators(session, overrideGlobalClaimValidators, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const claimValidatorsAddedByOtherRecipes = recipe_1.default - .getInstanceOrThrowError() - .getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators = yield recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getGlobalClaimValidators({ - userId: session.getUserId(), - claimValidatorsAddedByOtherRecipes, - userContext, - }); - return overrideGlobalClaimValidators !== undefined - ? yield overrideGlobalClaimValidators(globalClaimValidators, session, userContext) - : globalClaimValidators; - }); -} -exports.getRequiredClaimValidators = getRequiredClaimValidators; -function validateClaimsInPayload(claimValidators, newAccessTokenPayload, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const validationErrors = []; - for (const validator of claimValidators) { - const claimValidationResult = yield validator.validate(newAccessTokenPayload, userContext); - logger_1.logDebugMessage( - "validateClaimsInPayload " + validator.id + " validation res " + JSON.stringify(claimValidationResult) - ); - if (!claimValidationResult.isValid) { - validationErrors.push({ - id: validator.id, - reason: claimValidationResult.reason, - }); - } - } - return validationErrors; - }); -} -exports.validateClaimsInPayload = validateClaimsInPayload; -function defaultGetTokenTransferMethod({ req, forCreateNewSession }) { - // We allow fallback (checking headers then cookies) by default when validating - if (!forCreateNewSession) { - return "any"; - } - // In create new session we respect the frontend preference by default - switch (cookieAndHeaders_1.getAuthModeFromHeader(req)) { - case "header": - return "header"; - case "cookie": - return "cookie"; - default: - return "any"; - } -} diff --git a/lib/build/recipe/session/with-jwt/constants.d.ts b/lib/build/recipe/session/with-jwt/constants.d.ts deleted file mode 100644 index 324030f7b..000000000 --- a/lib/build/recipe/session/with-jwt/constants.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -export declare const ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY = "_jwtPName"; -export declare const JWT_RESERVED_KEY_USE_ERROR_MESSAGE: string; diff --git a/lib/build/recipe/session/with-jwt/constants.js b/lib/build/recipe/session/with-jwt/constants.js deleted file mode 100644 index b3a616064..000000000 --- a/lib/build/recipe/session/with-jwt/constants.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.JWT_RESERVED_KEY_USE_ERROR_MESSAGE = exports.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -/* - This key is used to determine the property name used when adding the jwt to the access token payload - For example if the Session recipe is initialised with config - { - ... - jwt: { - enable: true, - propertyNameInAccessTokenPayload: "jwtKey", - }, - ... - } - - The access token payload after creating a session would look like - { - ... - jwtKey: "JWT_STRING", - _jwtPName: "jwtKey", - } - - When trying to refresh the session or updating the access token payload, this key is used to determine and retrieve - the exsiting JWT from the access token payload. -*/ -exports.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY = "_jwtPName"; -exports.JWT_RESERVED_KEY_USE_ERROR_MESSAGE = `${exports.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY} is a reserved property name, please use a different key name for the jwt`; diff --git a/lib/build/recipe/session/with-jwt/index.d.ts b/lib/build/recipe/session/with-jwt/index.d.ts deleted file mode 100644 index f8ccc90af..000000000 --- a/lib/build/recipe/session/with-jwt/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import OriginalImplementation from "./recipeImplementation"; -export default OriginalImplementation; diff --git a/lib/build/recipe/session/with-jwt/index.js b/lib/build/recipe/session/with-jwt/index.js deleted file mode 100644 index 6b58cbc0b..000000000 --- a/lib/build/recipe/session/with-jwt/index.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -exports.default = recipeImplementation_1.default; diff --git a/lib/build/recipe/session/with-jwt/recipeImplementation.d.ts b/lib/build/recipe/session/with-jwt/recipeImplementation.d.ts deleted file mode 100644 index debe5ce4b..000000000 --- a/lib/build/recipe/session/with-jwt/recipeImplementation.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "../"; -import { RecipeInterface as OpenIdRecipeInterface } from "../../openid/types"; -import { TypeNormalisedInput } from "../types"; -export declare function setJWTExpiryOffsetSecondsForTesting(offset: number): void; -export default function ( - originalImplementation: RecipeInterface, - openIdRecipeImplementation: OpenIdRecipeInterface, - config: TypeNormalisedInput -): RecipeInterface; diff --git a/lib/build/recipe/session/with-jwt/recipeImplementation.js b/lib/build/recipe/session/with-jwt/recipeImplementation.js deleted file mode 100644 index 591cab341..000000000 --- a/lib/build/recipe/session/with-jwt/recipeImplementation.js +++ /dev/null @@ -1,242 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.setJWTExpiryOffsetSecondsForTesting = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const jsonwebtoken_1 = require("jsonwebtoken"); -const constants_1 = require("./constants"); -const sessionClass_1 = __importDefault(require("./sessionClass")); -const assert = __importStar(require("assert")); -const utils_1 = require("./utils"); -// Time difference between JWT expiry and access token expiry (JWT expiry = access token expiry + EXPIRY_OFFSET_SECONDS) -let EXPIRY_OFFSET_SECONDS = 30; -// This function should only be used during testing -function setJWTExpiryOffsetSecondsForTesting(offset) { - if (process.env.TEST_MODE !== "testing") { - throw Error("calling testing function in non testing env"); - } - EXPIRY_OFFSET_SECONDS = offset; -} -exports.setJWTExpiryOffsetSecondsForTesting = setJWTExpiryOffsetSecondsForTesting; -function default_1(originalImplementation, openIdRecipeImplementation, config) { - function getJWTExpiry(accessTokenExpiry) { - return accessTokenExpiry + EXPIRY_OFFSET_SECONDS; - } - function jwtAwareUpdateAccessTokenPayload(sessionInformation, newAccessTokenPayload, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let accessTokenPayload = sessionInformation.accessTokenPayload; - let existingJwtPropertyName = accessTokenPayload[constants_1.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - if (existingJwtPropertyName === undefined) { - return yield originalImplementation.updateAccessTokenPayload({ - sessionHandle: sessionInformation.sessionHandle, - newAccessTokenPayload, - userContext, - }); - } - let existingJwt = accessTokenPayload[existingJwtPropertyName]; - assert.notStrictEqual(existingJwt, undefined); - let currentTimeInSeconds = Date.now() / 1000; - let decodedPayload = jsonwebtoken_1.decode(existingJwt, { json: true }); - // decode possibly returns null - if (decodedPayload === null || decodedPayload.exp === undefined) { - throw new Error("Error reading JWT from session"); - } - let jwtExpiry = decodedPayload.exp - currentTimeInSeconds; - if (jwtExpiry <= 0) { - // it can come here if someone calls this function well after - // the access token and the jwt payload have expired. In this case, - // we still want the jwt payload to update, but the resulting JWT should - // not be alive for too long (since it's expired already). So we set it to - // 1 second lifetime. - jwtExpiry = 1; - } - newAccessTokenPayload = yield utils_1.addJWTToAccessTokenPayload({ - accessTokenPayload: newAccessTokenPayload, - jwtExpiry, - userId: sessionInformation.userId, - jwtPropertyName: existingJwtPropertyName, - openIdRecipeImplementation, - userContext, - }); - return yield originalImplementation.updateAccessTokenPayload({ - sessionHandle: sessionInformation.sessionHandle, - newAccessTokenPayload, - userContext, - }); - }); - } - return Object.assign(Object.assign({}, originalImplementation), { - createNewSession: function ({ req, res, userId, accessTokenPayload, sessionData, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - accessTokenPayload = - accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; - let accessTokenValidityInSeconds = Math.ceil( - (yield this.getAccessTokenLifeTimeMS({ userContext })) / 1000 - ); - accessTokenPayload = yield utils_1.addJWTToAccessTokenPayload({ - accessTokenPayload, - jwtExpiry: getJWTExpiry(accessTokenValidityInSeconds), - userId, - jwtPropertyName: config.jwt.propertyNameInAccessTokenPayload, - openIdRecipeImplementation, - userContext, - }); - let sessionContainer = yield originalImplementation.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - userContext, - }); - return new sessionClass_1.default(sessionContainer, openIdRecipeImplementation); - }); - }, - getSession: function ({ req, res, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let sessionContainer = yield originalImplementation.getSession({ req, res, options, userContext }); - if (sessionContainer === undefined) { - return undefined; - } - return new sessionClass_1.default(sessionContainer, openIdRecipeImplementation); - }); - }, - refreshSession: function ({ req, res, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let accessTokenValidityInSeconds = Math.ceil( - (yield this.getAccessTokenLifeTimeMS({ userContext })) / 1000 - ); - // Refresh session first because this will create a new access token - let newSession = yield originalImplementation.refreshSession({ req, res, userContext }); - let accessTokenPayload = newSession.getAccessTokenPayload(); - accessTokenPayload = yield utils_1.addJWTToAccessTokenPayload({ - accessTokenPayload, - jwtExpiry: getJWTExpiry(accessTokenValidityInSeconds), - userId: newSession.getUserId(), - jwtPropertyName: config.jwt.propertyNameInAccessTokenPayload, - openIdRecipeImplementation, - userContext, - }); - yield newSession.updateAccessTokenPayload(accessTokenPayload); - return new sessionClass_1.default(newSession, openIdRecipeImplementation); - }); - }, - mergeIntoAccessTokenPayload: function ({ sessionHandle, accessTokenPayloadUpdate, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let sessionInformation = yield this.getSessionInformation({ sessionHandle, userContext }); - if (!sessionInformation) { - return false; - } - let newAccessTokenPayload = Object.assign( - Object.assign({}, sessionInformation.accessTokenPayload), - accessTokenPayloadUpdate - ); - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete newAccessTokenPayload[key]; - } - } - return jwtAwareUpdateAccessTokenPayload(sessionInformation, newAccessTokenPayload, userContext); - }); - }, - /** - * @deprecated use mergeIntoAccessTokenPayload instead - */ - updateAccessTokenPayload: function ({ sessionHandle, newAccessTokenPayload, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - newAccessTokenPayload = - newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - const sessionInformation = yield this.getSessionInformation({ sessionHandle, userContext }); - if (!sessionInformation) { - return false; - } - return jwtAwareUpdateAccessTokenPayload(sessionInformation, newAccessTokenPayload, userContext); - }); - }, - }); -} -exports.default = default_1; diff --git a/lib/build/recipe/session/with-jwt/sessionClass.d.ts b/lib/build/recipe/session/with-jwt/sessionClass.d.ts deleted file mode 100644 index d9db91598..000000000 --- a/lib/build/recipe/session/with-jwt/sessionClass.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck -import { RecipeInterface as OpenIdRecipeInterface } from "../../openid/types"; -import { SessionClaim, SessionClaimValidator, SessionContainerInterface } from "../types"; -export default class SessionClassWithJWT implements SessionContainerInterface { - private readonly originalSessionClass; - private readonly openIdRecipeImplementation; - constructor(originalSessionClass: SessionContainerInterface, openIdRecipeImplementation: OpenIdRecipeInterface); - revokeSession(userContext?: any): Promise; - getSessionData(userContext?: any): Promise; - updateSessionData(newSessionData: any, userContext?: any): Promise; - getUserId(userContext?: any): string; - getAccessTokenPayload(userContext?: any): any; - getHandle(userContext?: any): string; - getAccessToken(userContext?: any): string; - getTimeCreated(userContext?: any): Promise; - getExpiry(userContext?: any): Promise; - assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise; - fetchAndSetClaim(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any): Promise; - setClaimValue(this: SessionClassWithJWT, claim: SessionClaim, value: T, userContext?: any): Promise; - getClaimValue(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any): Promise; - removeClaim(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any): Promise; - mergeIntoAccessTokenPayload( - this: SessionClassWithJWT, - accessTokenPayloadUpdate: any, - userContext?: any - ): Promise; - /** - * @deprecated use mergeIntoAccessTokenPayload instead - */ - updateAccessTokenPayload( - this: SessionClassWithJWT, - newAccessTokenPayload: any | undefined, - userContext?: any - ): Promise; -} diff --git a/lib/build/recipe/session/with-jwt/sessionClass.js b/lib/build/recipe/session/with-jwt/sessionClass.js deleted file mode 100644 index 030197eb6..000000000 --- a/lib/build/recipe/session/with-jwt/sessionClass.js +++ /dev/null @@ -1,229 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const jsonwebtoken_1 = require("jsonwebtoken"); -const assert = __importStar(require("assert")); -const constants_1 = require("./constants"); -const utils_1 = require("./utils"); -const error_1 = __importDefault(require("../error")); -const recipe_1 = __importDefault(require("../recipe")); -class SessionClassWithJWT { - constructor(originalSessionClass, openIdRecipeImplementation) { - this.originalSessionClass = originalSessionClass; - this.openIdRecipeImplementation = openIdRecipeImplementation; - } - revokeSession(userContext) { - return this.originalSessionClass.revokeSession(userContext); - } - getSessionData(userContext) { - return this.originalSessionClass.getSessionData(userContext); - } - updateSessionData(newSessionData, userContext) { - return this.originalSessionClass.updateSessionData(newSessionData, userContext); - } - getUserId(userContext) { - return this.originalSessionClass.getUserId(userContext); - } - getAccessTokenPayload(userContext) { - return this.originalSessionClass.getAccessTokenPayload(userContext); - } - getHandle(userContext) { - return this.originalSessionClass.getHandle(userContext); - } - getAccessToken(userContext) { - return this.originalSessionClass.getAccessToken(userContext); - } - getTimeCreated(userContext) { - return this.originalSessionClass.getTimeCreated(userContext); - } - getExpiry(userContext) { - return this.originalSessionClass.getExpiry(userContext); - } - // We copy the implementation here, since we want to override updateAccessTokenPayload - assertClaims(claimValidators, userContext) { - return __awaiter(this, void 0, void 0, function* () { - let validateClaimResponse = yield recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.validateClaims({ - accessTokenPayload: this.getAccessTokenPayload(userContext), - userId: this.getUserId(userContext), - claimValidators, - userContext, - }); - if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { - yield this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); - } - if (validateClaimResponse.invalidClaims.length !== 0) { - throw new error_1.default({ - type: "INVALID_CLAIMS", - message: "INVALID_CLAIMS", - payload: validateClaimResponse.invalidClaims, - }); - } - }); - } - // We copy the implementation here, since we want to override updateAccessTokenPayload - fetchAndSetClaim(claim, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const update = yield claim.build(this.getUserId(userContext), userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - }); - } - // We copy the implementation here, since we want to override updateAccessTokenPayload - setClaimValue(claim, value, userContext) { - const update = claim.addToPayload_internal({}, value, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - // We copy the implementation here, since we want to override updateAccessTokenPayload - getClaimValue(claim, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return claim.getValueFromPayload(yield this.getAccessTokenPayload(userContext), userContext); - }); - } - // We copy the implementation here, since we want to override updateAccessTokenPayload - removeClaim(claim, userContext) { - const update = claim.removeFromPayloadByMerge_internal({}, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - // We copy the implementation here, since we want to override updateAccessTokenPayload - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const updatedPayload = Object.assign( - Object.assign({}, this.getAccessTokenPayload(userContext)), - accessTokenPayloadUpdate - ); - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete updatedPayload[key]; - } - } - yield this.updateAccessTokenPayload(updatedPayload, userContext); - }); - } - // TODO: figure out a proper way to override just this function - /** - * @deprecated use mergeIntoAccessTokenPayload instead - */ - updateAccessTokenPayload(newAccessTokenPayload, userContext) { - return __awaiter(this, void 0, void 0, function* () { - newAccessTokenPayload = - newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - let accessTokenPayload = this.getAccessTokenPayload(userContext); - let jwtPropertyName = accessTokenPayload[constants_1.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - if (jwtPropertyName === undefined) { - return this.originalSessionClass.updateAccessTokenPayload(newAccessTokenPayload, userContext); - } - let existingJWT = accessTokenPayload[jwtPropertyName]; - assert.notStrictEqual(existingJWT, undefined); - let currentTimeInSeconds = Date.now() / 1000; - let decodedPayload = jsonwebtoken_1.decode(existingJWT, { json: true }); - // JsonWebToken.decode possibly returns null - if (decodedPayload === null || decodedPayload.exp === undefined) { - throw new Error("Error reading JWT from session"); - } - let jwtExpiry = decodedPayload.exp - currentTimeInSeconds; - if (jwtExpiry <= 0) { - // it can come here if someone calls this function well after - // the access token and the jwt payload have expired (which can happen if an API takes a VERY long time). In this case, we still want the jwt payload to update, but the resulting JWT should - // not be alive for too long (since it's expired already). So we set it to - // 1 second lifetime. - jwtExpiry = 1; - } - newAccessTokenPayload = yield utils_1.addJWTToAccessTokenPayload({ - accessTokenPayload: newAccessTokenPayload, - jwtExpiry, - userId: this.getUserId(), - jwtPropertyName, - openIdRecipeImplementation: this.openIdRecipeImplementation, - userContext, - }); - yield this.originalSessionClass.updateAccessTokenPayload(newAccessTokenPayload, userContext); - }); - } -} -exports.default = SessionClassWithJWT; diff --git a/lib/build/recipe/session/with-jwt/utils.d.ts b/lib/build/recipe/session/with-jwt/utils.d.ts deleted file mode 100644 index 3a4b54185..000000000 --- a/lib/build/recipe/session/with-jwt/utils.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -import { RecipeInterface as OpenIdRecipeInterface } from "../../openid/types"; -export declare function addJWTToAccessTokenPayload({ - accessTokenPayload, - jwtExpiry, - userId, - jwtPropertyName, - openIdRecipeImplementation, - userContext, -}: { - accessTokenPayload: any; - jwtExpiry: number; - userId: string; - jwtPropertyName: string; - openIdRecipeImplementation: OpenIdRecipeInterface; - userContext: any; -}): Promise; diff --git a/lib/build/recipe/session/with-jwt/utils.js b/lib/build/recipe/session/with-jwt/utils.js deleted file mode 100644 index 5597b5ca0..000000000 --- a/lib/build/recipe/session/with-jwt/utils.js +++ /dev/null @@ -1,109 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.addJWTToAccessTokenPayload = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const constants_1 = require("./constants"); -function addJWTToAccessTokenPayload({ - accessTokenPayload, - jwtExpiry, - userId, - jwtPropertyName, - openIdRecipeImplementation, - userContext, -}) { - return __awaiter(this, void 0, void 0, function* () { - // If jwtPropertyName is not undefined it means that the JWT was added to the access token payload already - let existingJwtPropertyName = accessTokenPayload[constants_1.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - if (existingJwtPropertyName !== undefined) { - // Delete the old JWT and the old property name - delete accessTokenPayload[existingJwtPropertyName]; - delete accessTokenPayload[constants_1.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - } - // Create the JWT - let jwtResponse = yield openIdRecipeImplementation.createJWT({ - payload: Object.assign( - { - /* - We add our claims before the user provided ones so that if they use the same claims - then the final payload will use the values they provide - */ - sub: userId, - }, - accessTokenPayload - ), - validitySeconds: jwtExpiry, - userContext, - }); - if (jwtResponse.status === "UNSUPPORTED_ALGORITHM_ERROR") { - // Should never come here - throw new Error("JWT Signing algorithm not supported"); - } - // Add the jwt and the property name to the access token payload - accessTokenPayload = Object.assign(Object.assign({}, accessTokenPayload), { - /* - We add the JWT after the user defined keys because we want to make sure that it never - gets overwritten by a user defined key. Using the same key as the one configured (or defaulting) - for the JWT should be considered a dev error - - ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY indicates a reserved key used to determine the property name - with which the JWT is set, used to retrieve the JWT from the access token payload during refresg and - updateAccessTokenPayload - - Note: If the user has multiple overrides each with a unique propertyNameInAccessTokenPayload, the logic - for checking the existing JWT when refreshing the session or updating the access token payload will not work. - This is because even though the jwt itself would be created with unique property names, the _jwtPName value would - always be overwritten by the override that runs last and when retrieving the jwt using that key name it cannot be - guaranteed that the right JWT is returned. This case is considered to be a rare requirement and we assume - that users will not need multiple JWT representations of their access token payload. - */ - [jwtPropertyName]: jwtResponse.jwt, - [constants_1.ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]: jwtPropertyName, - }); - return accessTokenPayload; - }); -} -exports.addJWTToAccessTokenPayload = addJWTToAccessTokenPayload; diff --git a/lib/build/recipe/thirdparty/api/appleRedirect.d.ts b/lib/build/recipe/thirdparty/api/appleRedirect.d.ts deleted file mode 100644 index df930a9a1..000000000 --- a/lib/build/recipe/thirdparty/api/appleRedirect.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function appleRedirectHandler(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/thirdparty/api/appleRedirect.js b/lib/build/recipe/thirdparty/api/appleRedirect.js deleted file mode 100644 index caf7cad15..000000000 --- a/lib/build/recipe/thirdparty/api/appleRedirect.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -function appleRedirectHandler(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.appleRedirectHandlerPOST === undefined) { - return false; - } - let body = yield options.req.getFormData(); - let state = body.state; - let code = body.code; - // this will redirect the user... - yield apiImplementation.appleRedirectHandlerPOST({ - code, - state, - options, - userContext: utils_1.makeDefaultUserContextFromAPI(options.req), - }); - return true; - }); -} -exports.default = appleRedirectHandler; diff --git a/lib/build/recipe/thirdparty/api/authorisationUrl.d.ts b/lib/build/recipe/thirdparty/api/authorisationUrl.d.ts deleted file mode 100644 index 5603eb9c1..000000000 --- a/lib/build/recipe/thirdparty/api/authorisationUrl.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function authorisationUrlAPI(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/thirdparty/api/authorisationUrl.js b/lib/build/recipe/thirdparty/api/authorisationUrl.js deleted file mode 100644 index dc9c1d64d..000000000 --- a/lib/build/recipe/thirdparty/api/authorisationUrl.js +++ /dev/null @@ -1,85 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("../../../utils"); -const error_1 = __importDefault(require("../error")); -const utils_2 = require("../utils"); -const utils_3 = require("../../../utils"); -function authorisationUrlAPI(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.authorisationUrlGET === undefined) { - return false; - } - let thirdPartyId = options.req.getKeyValueFromQuery("thirdPartyId"); - if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the thirdPartyId as a GET param", - }); - } - let provider = utils_2.findRightProvider(options.providers, thirdPartyId, undefined); - if (provider === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "The third party provider " + thirdPartyId + ` seems to be missing from the backend configs.`, - }); - } - let result = yield apiImplementation.authorisationUrlGET({ - provider, - options, - userContext: utils_3.makeDefaultUserContextFromAPI(options.req), - }); - utils_1.send200Response(options.res, result); - return true; - }); -} -exports.default = authorisationUrlAPI; diff --git a/lib/build/recipe/thirdparty/api/implementation.d.ts b/lib/build/recipe/thirdparty/api/implementation.d.ts deleted file mode 100644 index a594ecb37..000000000 --- a/lib/build/recipe/thirdparty/api/implementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../"; -export default function getAPIInterface(): APIInterface; -export declare function getActualClientIdFromDevelopmentClientId(client_id: string): string; diff --git a/lib/build/recipe/thirdparty/api/implementation.js b/lib/build/recipe/thirdparty/api/implementation.js deleted file mode 100644 index da30ed4af..000000000 --- a/lib/build/recipe/thirdparty/api/implementation.js +++ /dev/null @@ -1,250 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getActualClientIdFromDevelopmentClientId = void 0; -const session_1 = __importDefault(require("../../session")); -const url_1 = require("url"); -const axios = __importStar(require("axios")); -const qs = __importStar(require("querystring")); -const recipe_1 = __importDefault(require("../../emailverification/recipe")); -function getAPIInterface() { - return { - authorisationUrlGET: function ({ provider, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let providerInfo = provider.get(undefined, undefined, userContext); - let params = {}; - let keys = Object.keys(providerInfo.authorisationRedirect.params); - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - let value = providerInfo.authorisationRedirect.params[key]; - params[key] = typeof value === "function" ? yield value(options.req.original) : value; - } - if ( - providerInfo.getRedirectURI !== undefined && - !isUsingDevelopmentClientId(providerInfo.getClientId(userContext)) - ) { - // the backend wants to set the redirectURI - so we set that here. - // we add the not development keys because the oauth provider will - // redirect to supertokens.io's URL which will redirect the app - // to the the user's website, which will handle the callback as usual. - // If we add this, then instead, the supertokens' site will redirect - // the user to this API layer, which is not needed. - params["redirect_uri"] = providerInfo.getRedirectURI(userContext); - } - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - params["actual_redirect_uri"] = providerInfo.authorisationRedirect.url; - Object.keys(params).forEach((key) => { - if (params[key] === providerInfo.getClientId(userContext)) { - params[key] = getActualClientIdFromDevelopmentClientId( - providerInfo.getClientId(userContext) - ); - } - }); - } - let paramsString = new url_1.URLSearchParams(params).toString(); - let url = `${providerInfo.authorisationRedirect.url}?${paramsString}`; - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - url = `${DEV_OAUTH_AUTHORIZATION_URL}?${paramsString}`; - } - return { - status: "OK", - url, - }; - }); - }, - signInUpPOST: function ({ provider, code, redirectURI, authCodeResponse, options, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let userInfo; - let accessTokenAPIResponse; - { - let providerInfo = provider.get(undefined, undefined, userContext); - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - redirectURI = DEV_OAUTH_REDIRECT_URL; - } else if (providerInfo.getRedirectURI !== undefined) { - // we overwrite the redirectURI provided by the frontend - // since the backend wants to take charge of setting this. - redirectURI = providerInfo.getRedirectURI(userContext); - } - } - let providerInfo = provider.get(redirectURI, code, userContext); - if (authCodeResponse !== undefined) { - accessTokenAPIResponse = { - data: authCodeResponse, - }; - } else { - // we should use code to get the authCodeResponse body - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - Object.keys(providerInfo.accessTokenAPI.params).forEach((key) => { - if (providerInfo.accessTokenAPI.params[key] === providerInfo.getClientId(userContext)) { - providerInfo.accessTokenAPI.params[key] = getActualClientIdFromDevelopmentClientId( - providerInfo.getClientId(userContext) - ); - } - }); - } - accessTokenAPIResponse = yield axios.default({ - method: "post", - url: providerInfo.accessTokenAPI.url, - data: qs.stringify(providerInfo.accessTokenAPI.params), - headers: { - "content-type": "application/x-www-form-urlencoded", - accept: "application/json", // few providers like github don't send back json response by default - }, - }); - } - userInfo = yield providerInfo.getProfileInfo(accessTokenAPIResponse.data, userContext); - let emailInfo = userInfo.email; - if (emailInfo === undefined) { - return { - status: "NO_EMAIL_GIVEN_BY_PROVIDER", - }; - } - let response = yield options.recipeImplementation.signInUp({ - thirdPartyId: provider.id, - thirdPartyUserId: userInfo.id, - email: emailInfo.id, - userContext, - }); - // we set the email as verified if already verified by the OAuth provider. - // This block was added because of https://github.com/supertokens/supertokens-core/issues/295 - if (emailInfo.isVerified) { - const emailVerificationInstance = recipe_1.default.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = yield emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: response.user.id, - email: response.user.email, - userContext, - } - ); - if (tokenResponse.status === "OK") { - yield emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - userContext, - }); - } - } - } - let session = yield session_1.default.createNewSession( - options.req, - options.res, - response.user.id, - {}, - {}, - userContext - ); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - session, - authCodeResponse: accessTokenAPIResponse.data, - }; - }); - }, - appleRedirectHandlerPOST: function ({ code, state, options }) { - return __awaiter(this, void 0, void 0, function* () { - const redirectURL = - options.appInfo.websiteDomain.getAsStringDangerous() + - options.appInfo.websiteBasePath.getAsStringDangerous() + - "/callback/apple?state=" + - state + - "&code=" + - code; - options.res.sendHTMLResponse( - `` - ); - }); - }, - }; -} -exports.default = getAPIInterface; -const DEV_OAUTH_AUTHORIZATION_URL = "https://supertokens.io/dev/oauth/redirect-to-provider"; -const DEV_OAUTH_REDIRECT_URL = "https://supertokens.io/dev/oauth/redirect-to-app"; -// If Third Party login is used with one of the following development keys, then the dev authorization url and the redirect url will be used. -const DEV_OAUTH_CLIENT_IDS = [ - "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - "467101b197249757c71f", // github -]; -const DEV_KEY_IDENTIFIER = "4398792-"; -function isUsingDevelopmentClientId(client_id) { - return client_id.startsWith(DEV_KEY_IDENTIFIER) || DEV_OAUTH_CLIENT_IDS.includes(client_id); -} -function getActualClientIdFromDevelopmentClientId(client_id) { - if (client_id.startsWith(DEV_KEY_IDENTIFIER)) { - return client_id.split(DEV_KEY_IDENTIFIER)[1]; - } - return client_id; -} -exports.getActualClientIdFromDevelopmentClientId = getActualClientIdFromDevelopmentClientId; diff --git a/lib/build/recipe/thirdparty/api/signinup.d.ts b/lib/build/recipe/thirdparty/api/signinup.d.ts deleted file mode 100644 index b8fc4501c..000000000 --- a/lib/build/recipe/thirdparty/api/signinup.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface, APIOptions } from "../"; -export default function signInUpAPI(apiImplementation: APIInterface, options: APIOptions): Promise; diff --git a/lib/build/recipe/thirdparty/api/signinup.js b/lib/build/recipe/thirdparty/api/signinup.js deleted file mode 100644 index c9e1c7bf2..000000000 --- a/lib/build/recipe/thirdparty/api/signinup.js +++ /dev/null @@ -1,141 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../error")); -const utils_1 = require("../../../utils"); -const utils_2 = require("../utils"); -const utils_3 = require("../../../utils"); -function signInUpAPI(apiImplementation, options) { - return __awaiter(this, void 0, void 0, function* () { - if (apiImplementation.signInUpPOST === undefined) { - return false; - } - let bodyParams = yield options.req.getJSONBody(); - let thirdPartyId = bodyParams.thirdPartyId; - let code = bodyParams.code === undefined ? "" : bodyParams.code; - let redirectURI = bodyParams.redirectURI; - let authCodeResponse = bodyParams.authCodeResponse; - let clientId = bodyParams.clientId; - if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the thirdPartyId in request body", - }); - } - if (typeof code !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please make sure that the code in the request body is a string", - }); - } - if (code === "" && authCodeResponse === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide one of code or authCodeResponse in the request body", - }); - } - if (authCodeResponse !== undefined && authCodeResponse.access_token === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the access_token inside the authCodeResponse request param", - }); - } - if (redirectURI === undefined || typeof redirectURI !== "string") { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: "Please provide the redirectURI in request body", - }); - } - let provider = utils_2.findRightProvider(options.providers, thirdPartyId, clientId); - if (provider === undefined) { - if (clientId === undefined) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: - "The third party provider " + thirdPartyId + ` seems to be missing from the backend configs.`, - }); - } else { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: - "The third party provider " + - thirdPartyId + - ` seems to be missing from the backend configs. If it is configured, then please make sure that you are passing the correct clientId from the frontend.`, - }); - } - } - let result = yield apiImplementation.signInUpPOST({ - provider, - code, - clientId, - redirectURI, - options, - authCodeResponse, - userContext: utils_3.makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - utils_1.send200Response(options.res, { - status: result.status, - user: result.user, - createdNewUser: result.createdNewUser, - }); - } else if (result.status === "NO_EMAIL_GIVEN_BY_PROVIDER") { - utils_1.send200Response(options.res, { - status: "NO_EMAIL_GIVEN_BY_PROVIDER", - }); - } else { - utils_1.send200Response(options.res, result); - } - return true; - }); -} -exports.default = signInUpAPI; diff --git a/lib/build/recipe/thirdparty/constants.d.ts b/lib/build/recipe/thirdparty/constants.d.ts deleted file mode 100644 index e2235e1bf..000000000 --- a/lib/build/recipe/thirdparty/constants.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -export declare const AUTHORISATION_API = "/authorisationurl"; -export declare const SIGN_IN_UP_API = "/signinup"; -export declare const APPLE_REDIRECT_HANDLER = "/callback/apple"; diff --git a/lib/build/recipe/thirdparty/constants.js b/lib/build/recipe/thirdparty/constants.js deleted file mode 100644 index 531579d94..000000000 --- a/lib/build/recipe/thirdparty/constants.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APPLE_REDIRECT_HANDLER = exports.SIGN_IN_UP_API = exports.AUTHORISATION_API = void 0; -exports.AUTHORISATION_API = "/authorisationurl"; -exports.SIGN_IN_UP_API = "/signinup"; -exports.APPLE_REDIRECT_HANDLER = "/callback/apple"; diff --git a/lib/build/recipe/thirdparty/error.d.ts b/lib/build/recipe/thirdparty/error.d.ts deleted file mode 100644 index cc9e34552..000000000 --- a/lib/build/recipe/thirdparty/error.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -export default class ThirdPartyError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }); -} diff --git a/lib/build/recipe/thirdparty/error.js b/lib/build/recipe/thirdparty/error.js deleted file mode 100644 index 49fa39f33..000000000 --- a/lib/build/recipe/thirdparty/error.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class ThirdPartyError extends error_1.default { - constructor(options) { - super(Object.assign({}, options)); - this.fromRecipe = "thirdparty"; - } -} -exports.default = ThirdPartyError; diff --git a/lib/build/recipe/thirdparty/index.d.ts b/lib/build/recipe/thirdparty/index.d.ts deleted file mode 100644 index 4c076142b..000000000 --- a/lib/build/recipe/thirdparty/index.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { RecipeInterface, User, APIInterface, APIOptions, TypeProvider } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static Error: typeof SuperTokensError; - static signInUp( - thirdPartyId: string, - thirdPartyUserId: string, - email: string, - userContext?: any - ): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - static getUserById(userId: string, userContext?: any): Promise; - static getUsersByEmail(email: string, userContext?: any): Promise; - static getUserByThirdPartyInfo( - thirdPartyId: string, - thirdPartyUserId: string, - userContext?: any - ): Promise; - static Google: typeof import("./providers/google").default; - static Github: typeof import("./providers/github").default; - static Facebook: typeof import("./providers/facebook").default; - static Apple: typeof import("./providers/apple").default; - static Discord: typeof import("./providers/discord").default; - static GoogleWorkspaces: typeof import("./providers/googleWorkspaces").default; - static Bitbucket: typeof import("./providers/bitbucket").default; - static GitLab: typeof import("./providers/gitlab").default; -} -export declare let init: typeof Recipe.init; -export declare let Error: typeof SuperTokensError; -export declare let signInUp: typeof Wrapper.signInUp; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUsersByEmail: typeof Wrapper.getUsersByEmail; -export declare let getUserByThirdPartyInfo: typeof Wrapper.getUserByThirdPartyInfo; -export declare let Google: typeof import("./providers/google").default; -export declare let Github: typeof import("./providers/github").default; -export declare let Facebook: typeof import("./providers/facebook").default; -export declare let Apple: typeof import("./providers/apple").default; -export declare let Discord: typeof import("./providers/discord").default; -export declare let GoogleWorkspaces: typeof import("./providers/googleWorkspaces").default; -export declare let Bitbucket: typeof import("./providers/bitbucket").default; -export declare let GitLab: typeof import("./providers/gitlab").default; -export type { RecipeInterface, User, APIInterface, APIOptions, TypeProvider }; diff --git a/lib/build/recipe/thirdparty/index.js b/lib/build/recipe/thirdparty/index.js deleted file mode 100644 index a643b1b06..000000000 --- a/lib/build/recipe/thirdparty/index.js +++ /dev/null @@ -1,142 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.GitLab = exports.Bitbucket = exports.GoogleWorkspaces = exports.Discord = exports.Apple = exports.Facebook = exports.Github = exports.Google = exports.getUserByThirdPartyInfo = exports.getUsersByEmail = exports.getUserById = exports.signInUp = exports.Error = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const error_1 = __importDefault(require("./error")); -const thirdPartyProviders = __importStar(require("./providers")); -class Wrapper { - static signInUp(thirdPartyId, thirdPartyUserId, email, userContext = {}) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.signInUp({ - thirdPartyId, - thirdPartyUserId, - email, - userContext, - }); - }); - } - static getUserById(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - static getUsersByEmail(email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } - static getUserByThirdPartyInfo(thirdPartyId, thirdPartyUserId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - userContext, - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -Wrapper.Error = error_1.default; -Wrapper.Google = thirdPartyProviders.Google; -Wrapper.Github = thirdPartyProviders.Github; -Wrapper.Facebook = thirdPartyProviders.Facebook; -Wrapper.Apple = thirdPartyProviders.Apple; -Wrapper.Discord = thirdPartyProviders.Discord; -Wrapper.GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces; -Wrapper.Bitbucket = thirdPartyProviders.Bitbucket; -Wrapper.GitLab = thirdPartyProviders.GitLab; -exports.init = Wrapper.init; -exports.Error = Wrapper.Error; -exports.signInUp = Wrapper.signInUp; -exports.getUserById = Wrapper.getUserById; -exports.getUsersByEmail = Wrapper.getUsersByEmail; -exports.getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; -exports.Google = Wrapper.Google; -exports.Github = Wrapper.Github; -exports.Facebook = Wrapper.Facebook; -exports.Apple = Wrapper.Apple; -exports.Discord = Wrapper.Discord; -exports.GoogleWorkspaces = Wrapper.GoogleWorkspaces; -exports.Bitbucket = Wrapper.Bitbucket; -exports.GitLab = Wrapper.GitLab; diff --git a/lib/build/recipe/thirdparty/providers/activeDirectory.d.ts b/lib/build/recipe/thirdparty/providers/activeDirectory.d.ts deleted file mode 100644 index f6617924f..000000000 --- a/lib/build/recipe/thirdparty/providers/activeDirectory.d.ts +++ /dev/null @@ -1 +0,0 @@ -// @ts-nocheck diff --git a/lib/build/recipe/thirdparty/providers/activeDirectory.js b/lib/build/recipe/thirdparty/providers/activeDirectory.js deleted file mode 100644 index 35dd3bb54..000000000 --- a/lib/build/recipe/thirdparty/providers/activeDirectory.js +++ /dev/null @@ -1,103 +0,0 @@ -"use strict"; -// /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. -// * -// * This software is licensed under the Apache License, Version 2.0 (the -// * "License") as published by the Apache Software Foundation. -// * -// * You may not use this file except in compliance with the License. You may -// * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// * License for the specific language governing permissions and limitations -// * under the License. -// */ -// import { TypeProvider, TypeProviderGetResponse } from "../types"; -// import { verifyIdTokenFromJWKSEndpoint } from "./utils"; -// import { getActualClientIdFromDevelopmentClientId } from "../api/implementation"; -// type TypeThirdPartyProviderActiveDirectoryWorkspacesConfig = { -// clientId: string; -// clientSecret: string; -// scope?: string[]; -// tenantId: string; -// authorisationRedirect?: { -// params?: { [key: string]: string | ((request: any) => string) }; -// }; -// isDefault?: boolean; -// }; -// export default function AD(config: TypeThirdPartyProviderActiveDirectoryWorkspacesConfig): TypeProvider { -// const id = "active-directory"; -// function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { -// let accessTokenAPIURL = `https://login.microsoftonline.com/${config.tenantId}/oauth2/v2.0/token`; -// let accessTokenAPIParams: { [key: string]: string } = { -// client_id: config.clientId, -// client_secret: config.clientSecret, -// grant_type: "authorization_code", -// }; -// if (authCodeFromRequest !== undefined) { -// accessTokenAPIParams.code = authCodeFromRequest; -// } -// if (redirectURI !== undefined) { -// accessTokenAPIParams.redirect_uri = redirectURI; -// } -// let authorisationRedirectURL = `https://login.microsoftonline.com/${config.tenantId}/oauth2/v2.0/authorize`; -// let scopes = ["email", "openid"]; -// if (config.scope !== undefined) { -// scopes = config.scope; -// scopes = Array.from(new Set(scopes)); -// } -// let additionalParams = -// config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined -// ? {} -// : config.authorisationRedirect.params; -// let authorizationRedirectParams: { [key: string]: string } = { -// scope: scopes.join(" "), -// response_type: "code", -// client_id: config.clientId, -// ...additionalParams, -// }; -// async function getProfileInfo(authCodeResponse: { id_token: string }) { -// let payload: any = await verifyIdTokenFromJWKSEndpoint( -// authCodeResponse.id_token, -// `https://login.microsoftonline.com/${config.tenantId}/discovery/v2.0/keys`, -// { -// audience: getActualClientIdFromDevelopmentClientId(config.clientId), -// issuer: [`https://login.microsoftonline.com/${config.tenantId}/v2.0`], -// } -// ); -// if (payload.email === undefined) { -// throw new Error("Could not get email. Please use a different login method"); -// } -// if (payload.tid !== config.tenantId) { -// throw new Error("Incorrect tenantId used for signing in."); -// } -// return { -// id: payload.sub, -// email: { -// id: "example@email.com", -// isVerified: true, -// }, -// }; -// } -// return { -// accessTokenAPI: { -// url: accessTokenAPIURL, -// params: accessTokenAPIParams, -// }, -// authorisationRedirect: { -// url: authorisationRedirectURL, -// params: authorizationRedirectParams, -// }, -// getProfileInfo, -// getClientId: () => { -// return config.clientId; -// }, -// }; -// } -// return { -// id, -// get, -// isDefault: config.isDefault, -// }; -// } diff --git a/lib/build/recipe/thirdparty/providers/apple.d.ts b/lib/build/recipe/thirdparty/providers/apple.d.ts deleted file mode 100644 index 5370e2111..000000000 --- a/lib/build/recipe/thirdparty/providers/apple.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderAppleConfig = { - clientId: string; - clientSecret: { - keyId: string; - privateKey: string; - teamId: string; - }; - scope?: string[]; - authorisationRedirect?: { - params?: { - [key: string]: string | ((request: any) => string); - }; - }; - isDefault?: boolean; -}; -export default function Apple(config: TypeThirdPartyProviderAppleConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/apple.js b/lib/build/recipe/thirdparty/providers/apple.js deleted file mode 100644 index 9ef569cc3..000000000 --- a/lib/build/recipe/thirdparty/providers/apple.js +++ /dev/null @@ -1,167 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const jsonwebtoken_1 = require("jsonwebtoken"); -const error_1 = __importDefault(require("../error")); -const implementation_1 = require("../api/implementation"); -const supertokens_1 = __importDefault(require("../../../supertokens")); -const constants_1 = require("../constants"); -const verify_apple_id_token_1 = __importDefault(require("verify-apple-id-token")); -function Apple(config) { - const id = "apple"; - function getClientSecret(clientId, keyId, teamId, privateKey) { - return jsonwebtoken_1.sign( - { - iss: teamId, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + 86400 * 180, - aud: "https://appleid.apple.com", - sub: implementation_1.getActualClientIdFromDevelopmentClientId(clientId), - }, - privateKey.replace(/\\n/g, "\n"), - { algorithm: "ES256", keyid: keyId } - ); - } - try { - // trying to generate a client secret, in case client has not passed the values correctly - getClientSecret( - config.clientId, - config.clientSecret.keyId, - config.clientSecret.teamId, - config.clientSecret.privateKey - ); - } catch (error) { - throw new error_1.default({ - type: error_1.default.BAD_INPUT_ERROR, - message: error.message, - }); - } - function get(redirectURI, authCodeFromRequest) { - let accessTokenAPIURL = "https://appleid.apple.com/auth/token"; - let clientSecret = getClientSecret( - config.clientId, - config.clientSecret.keyId, - config.clientSecret.teamId, - config.clientSecret.privateKey - ); - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://appleid.apple.com/auth/authorize"; - let scopes = ["email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams = Object.assign( - { scope: scopes.join(" "), response_mode: "form_post", response_type: "code", client_id: config.clientId }, - additionalParams - ); - function getProfileInfo(accessTokenAPIResponse) { - return __awaiter(this, void 0, void 0, function* () { - /* - - Verify the JWS E256 signature using the server’s public key - - Verify the nonce for the authentication - - Verify that the iss field contains https://appleid.apple.com - - Verify that the aud field is the developer’s client_id - - Verify that the time is earlier than the exp value of the token */ - const payload = yield verify_apple_id_token_1.default({ - idToken: accessTokenAPIResponse.id_token, - clientId: implementation_1.getActualClientIdFromDevelopmentClientId(config.clientId), - }); - if (payload === null) { - throw new Error("no user info found from user's id token received from apple"); - } - let id = payload.sub; - let email = payload.email; - let isVerified = payload.email_verified; - if (id === undefined || id === null) { - throw new Error("no user info found from user's id token received from apple"); - } - return { - id, - email: { - id: email, - isVerified, - }, - }; - }); - } - function getRedirectURI() { - let supertokens = supertokens_1.default.getInstanceOrThrowError(); - return ( - supertokens.appInfo.apiDomain.getAsStringDangerous() + - supertokens.appInfo.apiBasePath.getAsStringDangerous() + - constants_1.APPLE_REDIRECT_HANDLER - ); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - getRedirectURI, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = Apple; diff --git a/lib/build/recipe/thirdparty/providers/bitbucket.d.ts b/lib/build/recipe/thirdparty/providers/bitbucket.d.ts deleted file mode 100644 index 1938f6c32..000000000 --- a/lib/build/recipe/thirdparty/providers/bitbucket.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderBitbucketConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { - [key: string]: string | ((request: any) => string); - }; - }; - isDefault?: boolean; -}; -export default function Bitbucket(config: TypeThirdPartyProviderBitbucketConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/bitbucket.js b/lib/build/recipe/thirdparty/providers/bitbucket.js deleted file mode 100644 index 215e1acdc..000000000 --- a/lib/build/recipe/thirdparty/providers/bitbucket.js +++ /dev/null @@ -1,147 +0,0 @@ -"use strict"; -/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -function Bitbucket(config) { - const id = "bitbucket"; - function get(redirectURI, authCodeFromRequest) { - let accessTokenAPIURL = "https://bitbucket.org/site/oauth2/access_token"; - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://bitbucket.org/site/oauth2/authorize"; - let scopes = ["account", "email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams = Object.assign( - { scope: scopes.join(" "), access_type: "offline", response_type: "code", client_id: config.clientId }, - additionalParams - ); - function getProfileInfo(accessTokenAPIResponse) { - return __awaiter(this, void 0, void 0, function* () { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = yield axios_1.default({ - method: "get", - url: "https://api.bitbucket.org/2.0/user", - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - let id = userInfo.uuid; - let emailRes = yield axios_1.default({ - method: "get", - url: "https://api.bitbucket.org/2.0/user/emails", - headers: { - Authorization: authHeader, - }, - }); - let emailData = emailRes.data; - let email = undefined; - let isVerified = false; - emailData.values.forEach((emailInfo) => { - if (emailInfo.is_primary) { - email = emailInfo.email; - isVerified = emailInfo.is_confirmed; - } - }); - if (email === undefined) { - return { - id, - }; - } - return { - id, - email: { - id: email, - isVerified, - }, - }; - }); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = Bitbucket; diff --git a/lib/build/recipe/thirdparty/providers/discord.d.ts b/lib/build/recipe/thirdparty/providers/discord.d.ts deleted file mode 100644 index 926e9d737..000000000 --- a/lib/build/recipe/thirdparty/providers/discord.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderDiscordConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { - [key: string]: string | ((request: any) => string); - }; - }; - isDefault?: boolean; -}; -export default function Discord(config: TypeThirdPartyProviderDiscordConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/discord.js b/lib/build/recipe/thirdparty/providers/discord.js deleted file mode 100644 index ccb1f043e..000000000 --- a/lib/build/recipe/thirdparty/providers/discord.js +++ /dev/null @@ -1,114 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -function Discord(config) { - const id = "discord"; - function get(redirectURI, authCodeFromRequest) { - let accessTokenAPIURL = "https://discord.com/api/oauth2/token"; - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://discord.com/api/oauth2/authorize"; - let scopes = ["email", "identify"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams = Object.assign( - { scope: scopes.join(" "), client_id: config.clientId, response_type: "code" }, - additionalParams - ); - function getProfileInfo(accessTokenAPIResponse) { - return __awaiter(this, void 0, void 0, function* () { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = yield axios_1.default({ - method: "get", - url: "https://discord.com/api/users/@me", - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - return { - id: userInfo.id, - email: - userInfo.email === undefined - ? undefined - : { - id: userInfo.email, - isVerified: userInfo.verified, - }, - }; - }); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = Discord; diff --git a/lib/build/recipe/thirdparty/providers/facebook.d.ts b/lib/build/recipe/thirdparty/providers/facebook.d.ts deleted file mode 100644 index 71b678eee..000000000 --- a/lib/build/recipe/thirdparty/providers/facebook.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderFacebookConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - isDefault?: boolean; -}; -export default function Facebook(config: TypeThirdPartyProviderFacebookConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/facebook.js b/lib/build/recipe/thirdparty/providers/facebook.js deleted file mode 100644 index 1b735a43f..000000000 --- a/lib/build/recipe/thirdparty/providers/facebook.js +++ /dev/null @@ -1,115 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -function Facebook(config) { - const id = "facebook"; - function get(redirectURI, authCodeFromRequest) { - let accessTokenAPIURL = "https://graph.facebook.com/v9.0/oauth/access_token"; - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: config.clientSecret, - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://www.facebook.com/v9.0/dialog/oauth"; - let scopes = ["email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let authorizationRedirectParams = { - scope: scopes.join(" "), - response_type: "code", - client_id: config.clientId, - }; - function getProfileInfo(accessTokenAPIResponse) { - return __awaiter(this, void 0, void 0, function* () { - let accessToken = accessTokenAPIResponse.access_token; - let response = yield axios_1.default({ - method: "get", - url: "https://graph.facebook.com/me", - params: { - access_token: accessToken, - fields: "id,email", - format: "json", - }, - }); - let userInfo = response.data; - let id = userInfo.id; - let email = userInfo.email; - if (email === undefined || email === null) { - return { - id, - }; - } - return { - id, - email: { - id: email, - isVerified: true, - }, - }; - }); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = Facebook; diff --git a/lib/build/recipe/thirdparty/providers/github.d.ts b/lib/build/recipe/thirdparty/providers/github.d.ts deleted file mode 100644 index 105f3adae..000000000 --- a/lib/build/recipe/thirdparty/providers/github.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderGithubConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { - [key: string]: string | ((request: any) => string); - }; - }; - isDefault?: boolean; -}; -export default function Github(config: TypeThirdPartyProviderGithubConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/github.js b/lib/build/recipe/thirdparty/providers/github.js deleted file mode 100644 index c387c0f4d..000000000 --- a/lib/build/recipe/thirdparty/providers/github.js +++ /dev/null @@ -1,145 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -function Github(config) { - const id = "github"; - function get(redirectURI, authCodeFromRequest) { - let accessTokenAPIURL = "https://github.com/login/oauth/access_token"; - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: config.clientSecret, - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://github.com/login/oauth/authorize"; - let scopes = ["read:user", "user:email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams = Object.assign( - { scope: scopes.join(" "), client_id: config.clientId }, - additionalParams - ); - function getProfileInfo(accessTokenAPIResponse) { - return __awaiter(this, void 0, void 0, function* () { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = yield axios_1.default({ - method: "get", - url: "https://api.github.com/user", - headers: { - Authorization: authHeader, - Accept: "application/vnd.github.v3+json", - }, - }); - let emailsInfoResponse = yield axios_1.default({ - url: "https://api.github.com/user/emails", - headers: { - Authorization: authHeader, - Accept: "application/vnd.github.v3+json", - }, - }); - let userInfo = response.data; - let emailsInfo = emailsInfoResponse.data; - let id = userInfo.id.toString(); // github userId will be a number - /* - if user has choosen not to show their email publicly, userInfo here will - have email as null. So we instead get the info from the emails api and - use the email which is marked as primary one. - - Sample github response for email info - [ - { - email: '', - primary: true, - verified: true, - visibility: 'public' - } - ] - */ - let emailInfo = emailsInfo.find((e) => e.primary); - if (emailInfo === undefined) { - return { - id, - }; - } - let isVerified = emailInfo !== undefined ? emailInfo.verified : false; - return { - id, - email: - emailInfo.email === undefined - ? undefined - : { - id: emailInfo.email, - isVerified, - }, - }; - }); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = Github; diff --git a/lib/build/recipe/thirdparty/providers/gitlab.d.ts b/lib/build/recipe/thirdparty/providers/gitlab.d.ts deleted file mode 100644 index 304fde9ee..000000000 --- a/lib/build/recipe/thirdparty/providers/gitlab.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderGitLabConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { - [key: string]: string | ((request: any) => string); - }; - }; - gitlabBaseUrl?: string; - isDefault?: boolean; -}; -export default function GitLab(config: TypeThirdPartyProviderGitLabConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/gitlab.js b/lib/build/recipe/thirdparty/providers/gitlab.js deleted file mode 100644 index 733b1c510..000000000 --- a/lib/build/recipe/thirdparty/providers/gitlab.js +++ /dev/null @@ -1,138 +0,0 @@ -"use strict"; -/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -const normalisedURLDomain_1 = __importDefault(require("../../../normalisedURLDomain")); -function GitLab(config) { - const id = "gitlab"; - function get(redirectURI, authCodeFromRequest) { - let baseUrl = - config.gitlabBaseUrl === undefined - ? "https://gitlab.com" // no traling slash cause we add that in the path - : new normalisedURLDomain_1.default(config.gitlabBaseUrl).getAsStringDangerous(); - let accessTokenAPIURL = baseUrl + "/oauth/token"; - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = baseUrl + "/oauth/authorize"; - let scopes = ["read_user"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams = Object.assign( - { scope: scopes.join(" "), response_type: "code", client_id: config.clientId }, - additionalParams - ); - function getProfileInfo(accessTokenAPIResponse) { - return __awaiter(this, void 0, void 0, function* () { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = yield axios_1.default({ - method: "get", - url: baseUrl + "/api/v4/user", - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - let id = userInfo.id + ""; - let email = userInfo.email; - if (email === undefined || email === null) { - return { - id, - }; - } - let isVerified = userInfo.confirmed_at !== null && userInfo.confirmed_at !== undefined; - return { - id, - email: { - id: email, - isVerified, - }, - }; - }); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = GitLab; diff --git a/lib/build/recipe/thirdparty/providers/google.d.ts b/lib/build/recipe/thirdparty/providers/google.d.ts deleted file mode 100644 index 4baaaccce..000000000 --- a/lib/build/recipe/thirdparty/providers/google.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderGoogleConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { - [key: string]: string | ((request: any) => string); - }; - }; - isDefault?: boolean; -}; -export default function Google(config: TypeThirdPartyProviderGoogleConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/google.js b/lib/build/recipe/thirdparty/providers/google.js deleted file mode 100644 index a9a479c2f..000000000 --- a/lib/build/recipe/thirdparty/providers/google.js +++ /dev/null @@ -1,128 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const axios_1 = __importDefault(require("axios")); -function Google(config) { - const id = "google"; - function get(redirectURI, authCodeFromRequest) { - let accessTokenAPIURL = "https://oauth2.googleapis.com/token"; - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://accounts.google.com/o/oauth2/v2/auth"; - let scopes = ["https://www.googleapis.com/auth/userinfo.email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams = Object.assign( - { - scope: scopes.join(" "), - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - client_id: config.clientId, - }, - additionalParams - ); - function getProfileInfo(accessTokenAPIResponse) { - return __awaiter(this, void 0, void 0, function* () { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = yield axios_1.default({ - method: "get", - url: "https://www.googleapis.com/oauth2/v1/userinfo", - params: { - alt: "json", - }, - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - let id = userInfo.id; - let email = userInfo.email; - if (email === undefined || email === null) { - return { - id, - }; - } - let isVerified = userInfo.verified_email; - return { - id, - email: { - id: email, - isVerified, - }, - }; - }); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = Google; diff --git a/lib/build/recipe/thirdparty/providers/googleWorkspaces.d.ts b/lib/build/recipe/thirdparty/providers/googleWorkspaces.d.ts deleted file mode 100644 index cf313c6ac..000000000 --- a/lib/build/recipe/thirdparty/providers/googleWorkspaces.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -import { TypeProvider } from "../types"; -declare type TypeThirdPartyProviderGoogleWorkspacesConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - domain?: string; - authorisationRedirect?: { - params?: { - [key: string]: string | ((request: any) => string); - }; - }; - isDefault?: boolean; -}; -export default function GW(config: TypeThirdPartyProviderGoogleWorkspacesConfig): TypeProvider; -export {}; diff --git a/lib/build/recipe/thirdparty/providers/googleWorkspaces.js b/lib/build/recipe/thirdparty/providers/googleWorkspaces.js deleted file mode 100644 index 8a1e526f6..000000000 --- a/lib/build/recipe/thirdparty/providers/googleWorkspaces.js +++ /dev/null @@ -1,123 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("./utils"); -const implementation_1 = require("../api/implementation"); -function GW(config) { - const id = "google-workspaces"; - let domain = config.domain === undefined ? "*" : config.domain; - function get(redirectURI, authCodeFromRequest) { - let accessTokenAPIURL = "https://oauth2.googleapis.com/token"; - let accessTokenAPIParams = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://accounts.google.com/o/oauth2/v2/auth"; - let scopes = ["https://www.googleapis.com/auth/userinfo.email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams = Object.assign( - { - scope: scopes.join(" "), - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - client_id: config.clientId, - hd: domain, - }, - additionalParams - ); - function getProfileInfo(authCodeResponse) { - return __awaiter(this, void 0, void 0, function* () { - let payload = yield utils_1.verifyIdTokenFromJWKSEndpoint( - authCodeResponse.id_token, - "https://www.googleapis.com/oauth2/v3/certs", - { - audience: implementation_1.getActualClientIdFromDevelopmentClientId(config.clientId), - issuer: ["https://accounts.google.com", "accounts.google.com"], - } - ); - if (payload.email === undefined) { - throw new Error("Could not get email. Please use a different login method"); - } - if (payload.hd === undefined) { - throw new Error("Please use a Google Workspace ID to login"); - } - // if the domain is "*" in it, it means that any workspace email is allowed. - if (!domain.includes("*") && payload.hd !== domain) { - throw new Error("Please use emails from " + domain + " to login"); - } - return { - id: payload.sub, - email: { - id: payload.email, - isVerified: payload.email_verified, - }, - }; - }); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - return { - id, - get, - isDefault: config.isDefault, - }; -} -exports.default = GW; diff --git a/lib/build/recipe/thirdparty/providers/index.d.ts b/lib/build/recipe/thirdparty/providers/index.d.ts deleted file mode 100644 index 5b9d69353..000000000 --- a/lib/build/recipe/thirdparty/providers/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -import ProviderGoogle from "./google"; -import ProviderFacebook from "./facebook"; -import ProviderGithub from "./github"; -import ProviderApple from "./apple"; -import ProviderDiscord from "./discord"; -import ProviderGoogleWorkspaces from "./googleWorkspaces"; -import ProviderBitbucket from "./bitbucket"; -import ProviderGitlab from "./gitlab"; -export declare let Google: typeof ProviderGoogle; -export declare let Facebook: typeof ProviderFacebook; -export declare let Github: typeof ProviderGithub; -export declare let Apple: typeof ProviderApple; -export declare let Discord: typeof ProviderDiscord; -export declare let GoogleWorkspaces: typeof ProviderGoogleWorkspaces; -export declare let Bitbucket: typeof ProviderBitbucket; -export declare let GitLab: typeof ProviderGitlab; diff --git a/lib/build/recipe/thirdparty/providers/index.js b/lib/build/recipe/thirdparty/providers/index.js deleted file mode 100644 index 467358a5e..000000000 --- a/lib/build/recipe/thirdparty/providers/index.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.GitLab = exports.Bitbucket = exports.GoogleWorkspaces = exports.Discord = exports.Apple = exports.Github = exports.Facebook = exports.Google = void 0; -const google_1 = __importDefault(require("./google")); -const facebook_1 = __importDefault(require("./facebook")); -const github_1 = __importDefault(require("./github")); -const apple_1 = __importDefault(require("./apple")); -const discord_1 = __importDefault(require("./discord")); -// import ProviderOkta from "./okta"; -const googleWorkspaces_1 = __importDefault(require("./googleWorkspaces")); -// import ProviderAD from "./activeDirectory"; -const bitbucket_1 = __importDefault(require("./bitbucket")); -const gitlab_1 = __importDefault(require("./gitlab")); -exports.Google = google_1.default; -exports.Facebook = facebook_1.default; -exports.Github = github_1.default; -exports.Apple = apple_1.default; -exports.Discord = discord_1.default; -exports.GoogleWorkspaces = googleWorkspaces_1.default; -exports.Bitbucket = bitbucket_1.default; -exports.GitLab = gitlab_1.default; -// export let Okta = ProviderOkta; -// export let ActiveDirectory = ProviderAD; diff --git a/lib/build/recipe/thirdparty/providers/okta.d.ts b/lib/build/recipe/thirdparty/providers/okta.d.ts deleted file mode 100644 index f6617924f..000000000 --- a/lib/build/recipe/thirdparty/providers/okta.d.ts +++ /dev/null @@ -1 +0,0 @@ -// @ts-nocheck diff --git a/lib/build/recipe/thirdparty/providers/okta.js b/lib/build/recipe/thirdparty/providers/okta.js deleted file mode 100644 index 1ab55988c..000000000 --- a/lib/build/recipe/thirdparty/providers/okta.js +++ /dev/null @@ -1,105 +0,0 @@ -"use strict"; -// /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. -// * -// * This software is licensed under the Apache License, Version 2.0 (the -// * "License") as published by the Apache Software Foundation. -// * -// * You may not use this file except in compliance with the License. You may -// * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// * License for the specific language governing permissions and limitations -// * under the License. -// */ -// import { TypeProvider, TypeProviderGetResponse } from "../types"; -// import axios from "axios"; -// type TypeThirdPartyProviderOktaConfig = { -// clientId: string; -// clientSecret: string; -// scope?: string[]; -// authorisationRedirect?: { -// params?: { [key: string]: string | ((request: any) => string) }; -// }; -// oktaDomain: string; -// authorizationServerId?: string; -// isDefault?: boolean; -// }; -// export default function Okta(config: TypeThirdPartyProviderOktaConfig): TypeProvider { -// const id = "okta"; -// const authorizationServerId = config.authorizationServerId === undefined ? "default" : config.authorizationServerId; -// const baseUrl = `https://${config.oktaDomain}/oauth2/${authorizationServerId}`; -// function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { -// let accessTokenAPIURL = baseUrl + "/v1/token"; -// let accessTokenAPIParams: { [key: string]: string } = { -// client_id: config.clientId, -// client_secret: config.clientSecret, -// grant_type: "authorization_code", -// }; -// if (authCodeFromRequest !== undefined) { -// accessTokenAPIParams.code = authCodeFromRequest; -// } -// if (redirectURI !== undefined) { -// accessTokenAPIParams.redirect_uri = redirectURI; -// } -// let authorisationRedirectURL = baseUrl + "/v1/authorize"; -// let scopes = ["openid", "email"]; -// if (config.scope !== undefined) { -// scopes = config.scope; -// scopes = Array.from(new Set(scopes)); -// } -// let additionalParams = -// config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined -// ? {} -// : config.authorisationRedirect.params; -// let authorizationRedirectParams: { [key: string]: string } = { -// scope: scopes.join(" "), -// client_id: config.clientId, -// response_type: "code", -// ...additionalParams, -// }; -// async function getProfileInfo(accessTokenAPIResponse: { -// access_token: string; -// expires_in: number; -// token_type: string; -// }) { -// let accessToken = accessTokenAPIResponse.access_token; -// let authHeader = `Bearer ${accessToken}`; -// let response = await axios({ -// method: "get", -// url: baseUrl + "/v1/userinfo", -// headers: { -// Authorization: authHeader, -// }, -// }); -// let userInfo = response.data; -// return { -// id: userInfo.sub, -// email: { -// id: userInfo.email, -// isVerified: userInfo.email_verified, -// }, -// }; -// } -// return { -// accessTokenAPI: { -// url: accessTokenAPIURL, -// params: accessTokenAPIParams, -// }, -// authorisationRedirect: { -// url: authorisationRedirectURL, -// params: authorizationRedirectParams, -// }, -// getProfileInfo, -// getClientId: () => { -// return config.clientId; -// }, -// }; -// } -// return { -// id, -// get, -// isDefault: config.isDefault, -// }; -// } diff --git a/lib/build/recipe/thirdparty/providers/utils.d.ts b/lib/build/recipe/thirdparty/providers/utils.d.ts deleted file mode 100644 index c3d294df8..000000000 --- a/lib/build/recipe/thirdparty/providers/utils.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { VerifyOptions } from "jsonwebtoken"; -export declare function verifyIdTokenFromJWKSEndpoint( - idToken: string, - jwksUri: string, - otherOptions: VerifyOptions -): Promise; diff --git a/lib/build/recipe/thirdparty/providers/utils.js b/lib/build/recipe/thirdparty/providers/utils.js deleted file mode 100644 index fbbe76311..000000000 --- a/lib/build/recipe/thirdparty/providers/utils.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.verifyIdTokenFromJWKSEndpoint = void 0; -const jsonwebtoken_1 = require("jsonwebtoken"); -const jwks_rsa_1 = __importDefault(require("jwks-rsa")); -function verifyIdTokenFromJWKSEndpoint(idToken, jwksUri, otherOptions) { - return __awaiter(this, void 0, void 0, function* () { - const client = jwks_rsa_1.default({ - jwksUri, - }); - function getKey(header, callback) { - client.getSigningKey(header.kid, function (_, key) { - var signingKey = key.publicKey || key.rsaPublicKey; - callback(null, signingKey); - }); - } - let payload = yield new Promise((resolve, reject) => { - jsonwebtoken_1.verify(idToken, getKey, otherOptions, function (err, decoded) { - if (err) { - reject(err); - } else { - resolve(decoded); - } - }); - }); - return payload; - }); -} -exports.verifyIdTokenFromJWKSEndpoint = verifyIdTokenFromJWKSEndpoint; diff --git a/lib/build/recipe/thirdparty/recipe.d.ts b/lib/build/recipe/thirdparty/recipe.d.ts deleted file mode 100644 index 4c4f5dae3..000000000 --- a/lib/build/recipe/thirdparty/recipe.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import { TypeInput, TypeNormalisedInput, TypeProvider, RecipeInterface, APIInterface } from "./types"; -import STError from "./error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - providers: TypeProvider[]; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - _recipes: {}, - _ingredients: {} - ); - static init(config: TypeInput): RecipeListFunction; - static getInstanceOrThrowError(): Recipe; - static reset(): void; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - _path: NormalisedURLPath, - _method: HTTPMethod - ) => Promise; - handleError: (err: STError, _request: BaseRequest, _response: BaseResponse) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; - getEmailForUserId: GetEmailForUserIdFunc; -} diff --git a/lib/build/recipe/thirdparty/recipe.js b/lib/build/recipe/thirdparty/recipe.js deleted file mode 100644 index b60df47f8..000000000 --- a/lib/build/recipe/thirdparty/recipe.js +++ /dev/null @@ -1,191 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const utils_1 = require("./utils"); -const recipe_1 = __importDefault(require("../emailverification/recipe")); -const error_1 = __importDefault(require("./error")); -const constants_1 = require("./constants"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const signinup_1 = __importDefault(require("./api/signinup")); -const authorisationUrl_1 = __importDefault(require("./api/authorisationUrl")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const querier_1 = require("../../querier"); -const appleRedirect_1 = __importDefault(require("./api/appleRedirect")); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config, _recipes, _ingredients) { - super(recipeId, appInfo); - this.getAPIsHandled = () => { - return [ - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_IN_UP_API), - id: constants_1.SIGN_IN_UP_API, - disabled: this.apiImpl.signInUpPOST === undefined, - }, - { - method: "get", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.AUTHORISATION_API), - id: constants_1.AUTHORISATION_API, - disabled: this.apiImpl.authorisationUrlGET === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.APPLE_REDIRECT_HANDLER), - id: constants_1.APPLE_REDIRECT_HANDLER, - disabled: this.apiImpl.appleRedirectHandlerPOST === undefined, - }, - ]; - }; - this.handleAPIRequest = (id, req, res, _path, _method) => - __awaiter(this, void 0, void 0, function* () { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - providers: this.providers, - req, - res, - appInfo: this.getAppInfo(), - }; - if (id === constants_1.SIGN_IN_UP_API) { - return yield signinup_1.default(this.apiImpl, options); - } else if (id === constants_1.AUTHORISATION_API) { - return yield authorisationUrl_1.default(this.apiImpl, options); - } else if (id === constants_1.APPLE_REDIRECT_HANDLER) { - return yield appleRedirect_1.default(this.apiImpl, options); - } - return false; - }); - this.handleError = (err, _request, _response) => - __awaiter(this, void 0, void 0, function* () { - throw err; - }); - this.getAllCORSHeaders = () => { - return []; - }; - this.isErrorFromThisRecipe = (err) => { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - // helper functions... - this.getEmailForUserId = (userId, userContext) => - __awaiter(this, void 0, void 0, function* () { - let userInfo = yield this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }); - this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - this.providers = this.config.signInAndUpFeature.providers; - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = recipe_1.default.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe( - Recipe.RECIPE_ID, - appInfo, - isInServerlessEnv, - config, - {}, - { - emailDelivery: undefined, - } - ); - return Recipe.instance; - } else { - throw new Error("ThirdParty recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "thirdparty"; diff --git a/lib/build/recipe/thirdparty/recipeImplementation.d.ts b/lib/build/recipe/thirdparty/recipeImplementation.d.ts deleted file mode 100644 index 7d1180bfb..000000000 --- a/lib/build/recipe/thirdparty/recipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "./types"; -import { Querier } from "../../querier"; -export default function getRecipeImplementation(querier: Querier): RecipeInterface; diff --git a/lib/build/recipe/thirdparty/recipeImplementation.js b/lib/build/recipe/thirdparty/recipeImplementation.js deleted file mode 100644 index 5fe91ff97..000000000 --- a/lib/build/recipe/thirdparty/recipeImplementation.js +++ /dev/null @@ -1,94 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeImplementation(querier) { - return { - signInUp: function ({ thirdPartyId, thirdPartyUserId, email }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/signinup"), { - thirdPartyId, - thirdPartyUserId, - email: { id: email }, - }); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - }; - }); - }, - getUserById: function ({ userId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { - userId, - }); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; - } - }); - }, - getUsersByEmail: function ({ email }) { - return __awaiter(this, void 0, void 0, function* () { - const { users } = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/recipe/users/by-email"), - { - email, - } - ); - return users; - }); - }, - getUserByThirdPartyInfo: function ({ thirdPartyId, thirdPartyUserId }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { - thirdPartyId, - thirdPartyUserId, - }); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; - } - }); - }, - }; -} -exports.default = getRecipeImplementation; diff --git a/lib/build/recipe/thirdparty/types.d.ts b/lib/build/recipe/thirdparty/types.d.ts deleted file mode 100644 index 04fe306bb..000000000 --- a/lib/build/recipe/thirdparty/types.d.ts +++ /dev/null @@ -1,144 +0,0 @@ -// @ts-nocheck -import { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { GeneralErrorResponse } from "../../types"; -export declare type UserInfo = { - id: string; - email?: { - id: string; - isVerified: boolean; - }; -}; -export declare type TypeProviderGetResponse = { - accessTokenAPI: { - url: string; - params: { - [key: string]: string; - }; - }; - authorisationRedirect: { - url: string; - params: { - [key: string]: string | ((request: any) => string); - }; - }; - getProfileInfo: (authCodeResponse: any, userContext: any) => Promise; - getClientId: (userContext: any) => string; - getRedirectURI?: (userContext: any) => string; -}; -export declare type TypeProvider = { - id: string; - get: ( - redirectURI: string | undefined, - authCodeFromRequest: string | undefined, - userContext: any - ) => TypeProviderGetResponse; - isDefault?: boolean; -}; -export declare type User = { - id: string; - timeJoined: number; - email: string; - thirdParty: { - id: string; - userId: string; - }; -}; -export declare type TypeInputSignInAndUp = { - providers: TypeProvider[]; -}; -export declare type TypeNormalisedInputSignInAndUp = { - providers: TypeProvider[]; -}; -export declare type TypeInput = { - signInAndUpFeature: TypeInputSignInAndUp; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - signInAndUpFeature: TypeNormalisedInputSignInAndUp; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - getUsersByEmail(input: { email: string; userContext: any }): Promise; - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise; - signInUp(input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; -}; -export declare type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - providers: TypeProvider[]; - req: BaseRequest; - res: BaseResponse; - appInfo: NormalisedAppinfo; -}; -export declare type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - provider: TypeProvider; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - url: string; - } - | GeneralErrorResponse - >); - signInUpPOST: - | undefined - | ((input: { - provider: TypeProvider; - code: string; - redirectURI: string; - authCodeResponse?: any; - clientId?: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - authCodeResponse: any; - } - | { - status: "NO_EMAIL_GIVEN_BY_PROVIDER"; - } - | GeneralErrorResponse - >); - appleRedirectHandlerPOST: - | undefined - | ((input: { code: string; state: string; options: APIOptions; userContext: any }) => Promise); -}; diff --git a/lib/build/recipe/thirdparty/types.js b/lib/build/recipe/thirdparty/types.js deleted file mode 100644 index a098ca1d7..000000000 --- a/lib/build/recipe/thirdparty/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/thirdparty/utils.d.ts b/lib/build/recipe/thirdparty/utils.d.ts deleted file mode 100644 index 2c878466d..000000000 --- a/lib/build/recipe/thirdparty/utils.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import { TypeProvider } from "./types"; -import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - appInfo: NormalisedAppinfo, - config: TypeInput -): TypeNormalisedInput; -export declare function findRightProvider( - providers: TypeProvider[], - thirdPartyId: string, - clientId?: string -): TypeProvider | undefined; diff --git a/lib/build/recipe/thirdparty/utils.js b/lib/build/recipe/thirdparty/utils.js deleted file mode 100644 index ab88136b4..000000000 --- a/lib/build/recipe/thirdparty/utils.js +++ /dev/null @@ -1,95 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.findRightProvider = exports.validateAndNormaliseUserInput = void 0; -function validateAndNormaliseUserInput(appInfo, config) { - let signInAndUpFeature = validateAndNormaliseSignInAndUpConfig(appInfo, config.signInAndUpFeature); - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config.override - ); - return { - signInAndUpFeature, - override, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; -function findRightProvider(providers, thirdPartyId, clientId) { - return providers.find((p) => { - let id = p.id; - if (id !== thirdPartyId) { - return false; - } - // first if there is only one provider with thirdPartyId in the providers array, - let otherProvidersWithSameId = providers.filter((p1) => p1.id === id && p !== p1); - if (otherProvidersWithSameId.length === 0) { - // they we always return that. - return true; - } - // otherwise, we look for the isDefault provider if clientId is missing - if (clientId === undefined) { - return p.isDefault === true; - } - // otherwise, we return a provider that matches based on client ID as well. - return p.get(undefined, undefined, {}).getClientId({}) === clientId; - }); -} -exports.findRightProvider = findRightProvider; -function validateAndNormaliseSignInAndUpConfig(_, config) { - let providers = config.providers; - if (providers === undefined || providers.length === 0) { - throw new Error( - "thirdparty recipe requires atleast 1 provider to be passed in signInAndUpFeature.providers config" - ); - } - // we check if there are multiple providers with the same id that have isDefault as true. - // In this case, we want to throw an error.. - let isDefaultProvidersSet = new Set(); - let allProvidersSet = new Set(); - providers.forEach((p) => { - let id = p.id; - allProvidersSet.add(p.id); - let isDefault = p.isDefault; - if (isDefault === undefined) { - // if this id is not being used by any other provider, we treat this as the isDefault - let otherProvidersWithSameId = providers.filter((p1) => p1.id === id && p !== p1); - if (otherProvidersWithSameId.length === 0) { - // we treat this as the isDefault now... - isDefault = true; - } - } - if (isDefault) { - if (isDefaultProvidersSet.has(id)) { - throw new Error( - `You have provided multiple third party providers that have the id: "${id}" and are marked as "isDefault: true". Please only mark one of them as isDefault.` - ); - } - isDefaultProvidersSet.add(id); - } - }); - if (isDefaultProvidersSet.size !== allProvidersSet.size) { - // this means that there is no provider marked as isDefault - throw new Error( - `The providers array has multiple entries for the same third party provider. Please mark one of them as the default one by using "isDefault: true".` - ); - } - return { - providers, - }; -} diff --git a/lib/build/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.d.ts b/lib/build/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.d.ts deleted file mode 100644 index 74122e450..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../../emailpassword"; -import { APIInterface as ThirdPartyEmailPasswordAPIInterface } from "../"; -export default function getIterfaceImpl(apiImplmentation: ThirdPartyEmailPasswordAPIInterface): APIInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.js b/lib/build/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.js deleted file mode 100644 index c8d9d033a..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -function getIterfaceImpl(apiImplmentation) { - var _a, _b, _c, _d, _e; - return { - emailExistsGET: - (_a = apiImplmentation.emailPasswordEmailExistsGET) === null || _a === void 0 - ? void 0 - : _a.bind(apiImplmentation), - generatePasswordResetTokenPOST: - (_b = apiImplmentation.generatePasswordResetTokenPOST) === null || _b === void 0 - ? void 0 - : _b.bind(apiImplmentation), - passwordResetPOST: - (_c = apiImplmentation.passwordResetPOST) === null || _c === void 0 ? void 0 : _c.bind(apiImplmentation), - signInPOST: - (_d = apiImplmentation.emailPasswordSignInPOST) === null || _d === void 0 - ? void 0 - : _d.bind(apiImplmentation), - signUpPOST: - (_e = apiImplmentation.emailPasswordSignUpPOST) === null || _e === void 0 - ? void 0 - : _e.bind(apiImplmentation), - }; -} -exports.default = getIterfaceImpl; diff --git a/lib/build/recipe/thirdpartyemailpassword/api/implementation.d.ts b/lib/build/recipe/thirdpartyemailpassword/api/implementation.d.ts deleted file mode 100644 index 402db9918..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../"; -export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/api/implementation.js b/lib/build/recipe/thirdpartyemailpassword/api/implementation.js deleted file mode 100644 index a34e03443..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/api/implementation.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const implementation_1 = __importDefault(require("../../emailpassword/api/implementation")); -const implementation_2 = __importDefault(require("../../thirdparty/api/implementation")); -const emailPasswordAPIImplementation_1 = __importDefault(require("./emailPasswordAPIImplementation")); -const thirdPartyAPIImplementation_1 = __importDefault(require("./thirdPartyAPIImplementation")); -function getAPIImplementation() { - var _a, _b, _c, _d, _e, _f, _g, _h; - let emailPasswordImplementation = implementation_1.default(); - let thirdPartyImplementation = implementation_2.default(); - return { - emailPasswordEmailExistsGET: - (_a = emailPasswordImplementation.emailExistsGET) === null || _a === void 0 - ? void 0 - : _a.bind(emailPasswordAPIImplementation_1.default(this)), - authorisationUrlGET: - (_b = thirdPartyImplementation.authorisationUrlGET) === null || _b === void 0 - ? void 0 - : _b.bind(thirdPartyAPIImplementation_1.default(this)), - emailPasswordSignInPOST: - (_c = emailPasswordImplementation.signInPOST) === null || _c === void 0 - ? void 0 - : _c.bind(emailPasswordAPIImplementation_1.default(this)), - emailPasswordSignUpPOST: - (_d = emailPasswordImplementation.signUpPOST) === null || _d === void 0 - ? void 0 - : _d.bind(emailPasswordAPIImplementation_1.default(this)), - generatePasswordResetTokenPOST: - (_e = emailPasswordImplementation.generatePasswordResetTokenPOST) === null || _e === void 0 - ? void 0 - : _e.bind(emailPasswordAPIImplementation_1.default(this)), - passwordResetPOST: - (_f = emailPasswordImplementation.passwordResetPOST) === null || _f === void 0 - ? void 0 - : _f.bind(emailPasswordAPIImplementation_1.default(this)), - thirdPartySignInUpPOST: - (_g = thirdPartyImplementation.signInUpPOST) === null || _g === void 0 - ? void 0 - : _g.bind(thirdPartyAPIImplementation_1.default(this)), - appleRedirectHandlerPOST: - (_h = thirdPartyImplementation.appleRedirectHandlerPOST) === null || _h === void 0 - ? void 0 - : _h.bind(thirdPartyAPIImplementation_1.default(this)), - }; -} -exports.default = getAPIImplementation; diff --git a/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.d.ts b/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.d.ts deleted file mode 100644 index b0827c889..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../../thirdparty"; -import { APIInterface as ThirdPartyEmailPasswordAPIInterface } from "../"; -export default function getIterfaceImpl(apiImplmentation: ThirdPartyEmailPasswordAPIInterface): APIInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.js b/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.js deleted file mode 100644 index 0023e4411..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getIterfaceImpl(apiImplmentation) { - var _a, _b, _c; - const signInUpPOSTFromThirdPartyEmailPassword = - (_a = apiImplmentation.thirdPartySignInUpPOST) === null || _a === void 0 ? void 0 : _a.bind(apiImplmentation); - return { - authorisationUrlGET: - (_b = apiImplmentation.authorisationUrlGET) === null || _b === void 0 ? void 0 : _b.bind(apiImplmentation), - appleRedirectHandlerPOST: - (_c = apiImplmentation.appleRedirectHandlerPOST) === null || _c === void 0 - ? void 0 - : _c.bind(apiImplmentation), - signInUpPOST: - signInUpPOSTFromThirdPartyEmailPassword === undefined - ? undefined - : function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield signInUpPOSTFromThirdPartyEmailPassword(input); - if (result.status === "OK") { - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return Object.assign(Object.assign({}, result), { - user: Object.assign(Object.assign({}, result.user), { - thirdParty: Object.assign({}, result.user.thirdParty), - }), - }); - } - return result; - }); - }, - }; -} -exports.default = getIterfaceImpl; diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts deleted file mode 100644 index be35a1981..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -import { TypeThirdPartyEmailPasswordEmailDeliveryInput, User } from "../../../types"; -import { RecipeInterface as EmailPasswordRecipeInterface } from "../../../../emailpassword"; -import { NormalisedAppinfo } from "../../../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private emailPasswordBackwardCompatibilityService; - constructor( - emailPasswordRecipeInterfaceImpl: EmailPasswordRecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } - ); - sendEmail: ( - input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js deleted file mode 100644 index c4057c084..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const backwardCompatibility_1 = __importDefault( - require("../../../../emailpassword/emaildelivery/services/backwardCompatibility") -); -class BackwardCompatibilityService { - constructor(emailPasswordRecipeInterfaceImpl, appInfo, isInServerlessEnv, resetPasswordUsingTokenFeature) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.emailPasswordBackwardCompatibilityService.sendEmail(input); - }); - { - this.emailPasswordBackwardCompatibilityService = new backwardCompatibility_1.default( - emailPasswordRecipeInterfaceImpl, - appInfo, - isInServerlessEnv, - resetPasswordUsingTokenFeature - ); - } - } -} -exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/index.d.ts deleted file mode 100644 index 4de04d983..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import SMTP from "./smtp"; -export declare let SMTPService: typeof SMTP; diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/index.js b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/index.js deleted file mode 100644 index 7e07f6706..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/index.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SMTPService = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const smtp_1 = __importDefault(require("./smtp")); -exports.SMTPService = smtp_1.default; diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.d.ts deleted file mode 100644 index debae94a2..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; -export default class SMTPService implements EmailDeliveryInterface { - private emailPasswordSMTPService; - constructor(config: TypeInput); - sendEmail: ( - input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.js b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.js deleted file mode 100644 index ab6aa1bf8..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const smtp_1 = __importDefault(require("../../../../emailpassword/emaildelivery/services/smtp")); -class SMTPService { - constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.emailPasswordSMTPService.sendEmail(input); - }); - this.emailPasswordSMTPService = new smtp_1.default(config); - } -} -exports.default = SMTPService; diff --git a/lib/build/recipe/thirdpartyemailpassword/error.d.ts b/lib/build/recipe/thirdpartyemailpassword/error.d.ts deleted file mode 100644 index 1d9c33665..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/error.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -export default class ThirdPartyEmailPasswordError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }); -} diff --git a/lib/build/recipe/thirdpartyemailpassword/error.js b/lib/build/recipe/thirdpartyemailpassword/error.js deleted file mode 100644 index a16bebfe0..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/error.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class ThirdPartyEmailPasswordError extends error_1.default { - constructor(options) { - super(Object.assign({}, options)); - this.fromRecipe = "thirdpartyemailpassword"; - } -} -exports.default = ThirdPartyEmailPasswordError; diff --git a/lib/build/recipe/thirdpartyemailpassword/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/index.d.ts deleted file mode 100644 index d5ea98aa3..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/index.d.ts +++ /dev/null @@ -1,120 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { RecipeInterface, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions } from "./types"; -import { TypeProvider } from "../thirdparty/types"; -import { TypeEmailPasswordEmailDeliveryInput } from "../emailpassword/types"; -export default class Wrapper { - static init: typeof Recipe.init; - static Error: typeof SuperTokensError; - static thirdPartySignInUp( - thirdPartyId: string, - thirdPartyUserId: string, - email: string, - userContext?: any - ): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - static getUserByThirdPartyInfo( - thirdPartyId: string, - thirdPartyUserId: string, - userContext?: any - ): Promise; - static emailPasswordSignUp( - email: string, - password: string, - userContext?: any - ): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - >; - static emailPasswordSignIn( - email: string, - password: string, - userContext?: any - ): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - >; - static getUserById(userId: string, userContext?: any): Promise; - static getUsersByEmail(email: string, userContext?: any): Promise; - static createResetPasswordToken( - userId: string, - userContext?: any - ): Promise< - | { - status: "OK"; - token: string; - } - | { - status: "UNKNOWN_USER_ID_ERROR"; - } - >; - static resetPasswordUsingToken( - token: string, - newPassword: string, - userContext?: any - ): Promise< - | { - status: "OK"; - userId?: string | undefined; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - >; - static updateEmailOrPassword(input: { - userId: string; - email?: string; - password?: string; - userContext?: any; - }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR"; - }>; - static Google: typeof import("../thirdparty/providers/google").default; - static Github: typeof import("../thirdparty/providers/github").default; - static Facebook: typeof import("../thirdparty/providers/facebook").default; - static Apple: typeof import("../thirdparty/providers/apple").default; - static Discord: typeof import("../thirdparty/providers/discord").default; - static GoogleWorkspaces: typeof import("../thirdparty/providers/googleWorkspaces").default; - static Bitbucket: typeof import("../thirdparty/providers/bitbucket").default; - static GitLab: typeof import("../thirdparty/providers/gitlab").default; - static sendEmail( - input: TypeEmailPasswordEmailDeliveryInput & { - userContext?: any; - } - ): Promise; -} -export declare let init: typeof Recipe.init; -export declare let Error: typeof SuperTokensError; -export declare let emailPasswordSignUp: typeof Wrapper.emailPasswordSignUp; -export declare let emailPasswordSignIn: typeof Wrapper.emailPasswordSignIn; -export declare let thirdPartySignInUp: typeof Wrapper.thirdPartySignInUp; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByThirdPartyInfo: typeof Wrapper.getUserByThirdPartyInfo; -export declare let getUsersByEmail: typeof Wrapper.getUsersByEmail; -export declare let createResetPasswordToken: typeof Wrapper.createResetPasswordToken; -export declare let resetPasswordUsingToken: typeof Wrapper.resetPasswordUsingToken; -export declare let updateEmailOrPassword: typeof Wrapper.updateEmailOrPassword; -export declare let Google: typeof import("../thirdparty/providers/google").default; -export declare let Github: typeof import("../thirdparty/providers/github").default; -export declare let Facebook: typeof import("../thirdparty/providers/facebook").default; -export declare let Apple: typeof import("../thirdparty/providers/apple").default; -export declare let Discord: typeof import("../thirdparty/providers/discord").default; -export declare let GoogleWorkspaces: typeof import("../thirdparty/providers/googleWorkspaces").default; -export declare let Bitbucket: typeof import("../thirdparty/providers/bitbucket").default; -export declare let GitLab: typeof import("../thirdparty/providers/gitlab").default; -export type { RecipeInterface, TypeProvider, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions }; -export declare let sendEmail: typeof Wrapper.sendEmail; diff --git a/lib/build/recipe/thirdpartyemailpassword/index.js b/lib/build/recipe/thirdpartyemailpassword/index.js deleted file mode 100644 index fa2d0eed7..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/index.js +++ /dev/null @@ -1,186 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendEmail = exports.GitLab = exports.Bitbucket = exports.GoogleWorkspaces = exports.Discord = exports.Apple = exports.Facebook = exports.Github = exports.Google = exports.updateEmailOrPassword = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.getUsersByEmail = exports.getUserByThirdPartyInfo = exports.getUserById = exports.thirdPartySignInUp = exports.emailPasswordSignIn = exports.emailPasswordSignUp = exports.Error = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const error_1 = __importDefault(require("./error")); -const thirdPartyProviders = __importStar(require("../thirdparty/providers")); -class Wrapper { - static thirdPartySignInUp(thirdPartyId, thirdPartyUserId, email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartySignInUp({ - thirdPartyId, - thirdPartyUserId, - email, - userContext, - }); - } - static getUserByThirdPartyInfo(thirdPartyId, thirdPartyUserId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - userContext, - }); - } - static emailPasswordSignUp(email, password, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignUp({ - email, - password, - userContext, - }); - } - static emailPasswordSignIn(email, password, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignIn({ - email, - password, - userContext, - }); - } - static getUserById(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - static getUsersByEmail(email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } - static createResetPasswordToken(userId, userContext = {}) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createResetPasswordToken({ userId, userContext }); - } - static resetPasswordUsingToken(token, newPassword, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ - token, - newPassword, - userContext, - }); - } - static updateEmailOrPassword(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.updateEmailOrPassword(Object.assign({ userContext: {} }, input)); - } - // static Okta = thirdPartyProviders.Okta; - // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .emailDelivery.ingredientInterfaceImpl.sendEmail(Object.assign({ userContext: {} }, input)); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -Wrapper.Error = error_1.default; -Wrapper.Google = thirdPartyProviders.Google; -Wrapper.Github = thirdPartyProviders.Github; -Wrapper.Facebook = thirdPartyProviders.Facebook; -Wrapper.Apple = thirdPartyProviders.Apple; -Wrapper.Discord = thirdPartyProviders.Discord; -Wrapper.GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces; -Wrapper.Bitbucket = thirdPartyProviders.Bitbucket; -Wrapper.GitLab = thirdPartyProviders.GitLab; -exports.init = Wrapper.init; -exports.Error = Wrapper.Error; -exports.emailPasswordSignUp = Wrapper.emailPasswordSignUp; -exports.emailPasswordSignIn = Wrapper.emailPasswordSignIn; -exports.thirdPartySignInUp = Wrapper.thirdPartySignInUp; -exports.getUserById = Wrapper.getUserById; -exports.getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; -exports.getUsersByEmail = Wrapper.getUsersByEmail; -exports.createResetPasswordToken = Wrapper.createResetPasswordToken; -exports.resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; -exports.updateEmailOrPassword = Wrapper.updateEmailOrPassword; -exports.Google = Wrapper.Google; -exports.Github = Wrapper.Github; -exports.Facebook = Wrapper.Facebook; -exports.Apple = Wrapper.Apple; -exports.Discord = Wrapper.Discord; -exports.GoogleWorkspaces = Wrapper.GoogleWorkspaces; -exports.Bitbucket = Wrapper.Bitbucket; -exports.GitLab = Wrapper.GitLab; -exports.sendEmail = Wrapper.sendEmail; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts b/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts deleted file mode 100644 index 36494b074..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts +++ /dev/null @@ -1,60 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import EmailPasswordRecipe from "../emailpassword/recipe"; -import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; -import STError from "./error"; -import { - TypeInput, - TypeNormalisedInput, - RecipeInterface, - APIInterface, - TypeThirdPartyEmailPasswordEmailDeliveryInput, -} from "./types"; -import STErrorEmailPassword from "../emailpassword/error"; -import STErrorThirdParty from "../thirdparty/error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - private emailPasswordRecipe; - private thirdPartyRecipe; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - emailDelivery: EmailDeliveryIngredient; - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - recipes: { - thirdPartyInstance: ThirdPartyRecipe | undefined; - emailPasswordInstance: EmailPasswordRecipe | undefined; - }, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - } - ); - static init(config: TypeInput): RecipeListFunction; - static reset(): void; - static getInstanceOrThrowError(): Recipe; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ) => Promise; - handleError: ( - err: STErrorEmailPassword | STErrorThirdParty, - request: BaseRequest, - response: BaseResponse - ) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; -} diff --git a/lib/build/recipe/thirdpartyemailpassword/recipe.js b/lib/build/recipe/thirdpartyemailpassword/recipe.js deleted file mode 100644 index dceb0bd55..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipe.js +++ /dev/null @@ -1,243 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const recipe_1 = __importDefault(require("../emailpassword/recipe")); -const recipe_2 = __importDefault(require("../thirdparty/recipe")); -const error_1 = __importDefault(require("./error")); -const utils_1 = require("./utils"); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const emailPasswordRecipeImplementation_1 = __importDefault( - require("./recipeImplementation/emailPasswordRecipeImplementation") -); -const thirdPartyRecipeImplementation_1 = __importDefault( - require("./recipeImplementation/thirdPartyRecipeImplementation") -); -const thirdPartyAPIImplementation_1 = __importDefault(require("./api/thirdPartyAPIImplementation")); -const emailPasswordAPIImplementation_1 = __importDefault(require("./api/emailPasswordAPIImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const querier_1 = require("../../querier"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config, recipes, ingredients) { - super(recipeId, appInfo); - this.getAPIsHandled = () => { - let apisHandled = [...this.emailPasswordRecipe.getAPIsHandled()]; - if (this.thirdPartyRecipe !== undefined) { - apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()); - } - return apisHandled; - }; - this.handleAPIRequest = (id, req, res, path, method) => - __awaiter(this, void 0, void 0, function* () { - if (this.emailPasswordRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined) { - return yield this.emailPasswordRecipe.handleAPIRequest(id, req, res, path, method); - } - if ( - this.thirdPartyRecipe !== undefined && - this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined - ) { - return yield this.thirdPartyRecipe.handleAPIRequest(id, req, res, path, method); - } - return false; - }); - this.handleError = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === Recipe.RECIPE_ID) { - throw err; - } else { - if (this.emailPasswordRecipe.isErrorFromThisRecipe(err)) { - return yield this.emailPasswordRecipe.handleError(err, request, response); - } else if ( - this.thirdPartyRecipe !== undefined && - this.thirdPartyRecipe.isErrorFromThisRecipe(err) - ) { - return yield this.thirdPartyRecipe.handleError(err, request, response); - } - throw err; - } - }); - this.getAllCORSHeaders = () => { - let corsHeaders = [...this.emailPasswordRecipe.getAllCORSHeaders()]; - if (this.thirdPartyRecipe !== undefined) { - corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()); - } - return corsHeaders; - }; - this.isErrorFromThisRecipe = (err) => { - return ( - error_1.default.isErrorFromSuperTokens(err) && - (err.fromRecipe === Recipe.RECIPE_ID || - this.emailPasswordRecipe.isErrorFromThisRecipe(err) || - (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err))) - ); - }; - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default( - querier_1.Querier.getNewInstanceOrThrowError(recipe_1.default.RECIPE_ID), - querier_1.Querier.getNewInstanceOrThrowError(recipe_2.default.RECIPE_ID) - ) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - let emailPasswordRecipeImplementation = emailPasswordRecipeImplementation_1.default(this.recipeInterfaceImpl); - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new emaildelivery_1.default( - this.config.getEmailDeliveryConfig(emailPasswordRecipeImplementation, this.isInServerlessEnv) - ) - : ingredients.emailDelivery; - this.emailPasswordRecipe = - recipes.emailPasswordInstance !== undefined - ? recipes.emailPasswordInstance - : new recipe_1.default( - recipeId, - appInfo, - isInServerlessEnv, - { - override: { - functions: (_) => { - return emailPasswordRecipeImplementation; - }, - apis: (_) => { - return emailPasswordAPIImplementation_1.default(this.apiImpl); - }, - }, - signUpFeature: { - formFields: this.config.signUpFeature.formFields, - }, - resetPasswordUsingTokenFeature: this.config.resetPasswordUsingTokenFeature, - }, - { - emailDelivery: this.emailDelivery, - } - ); - if (this.config.providers.length !== 0) { - this.thirdPartyRecipe = - recipes.thirdPartyInstance !== undefined - ? recipes.thirdPartyInstance - : new recipe_2.default( - recipeId, - appInfo, - isInServerlessEnv, - { - override: { - functions: (_) => { - return thirdPartyRecipeImplementation_1.default(this.recipeInterfaceImpl); - }, - apis: (_) => { - return thirdPartyAPIImplementation_1.default(this.apiImpl); - }, - }, - signInAndUpFeature: { - providers: this.config.providers, - }, - }, - {}, - { - emailDelivery: this.emailDelivery, - } - ); - } - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe( - Recipe.RECIPE_ID, - appInfo, - isInServerlessEnv, - config, - { - emailPasswordInstance: undefined, - thirdPartyInstance: undefined, - }, - { - emailDelivery: undefined, - } - ); - return Recipe.instance; - } else { - throw new Error( - "ThirdPartyEmailPassword recipe has already been initialised. Please check your code for bugs." - ); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "thirdpartyemailpassword"; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.d.ts b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.d.ts deleted file mode 100644 index 26119b84e..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "../../emailpassword/types"; -import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; -export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js deleted file mode 100644 index 8d8772bdd..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js +++ /dev/null @@ -1,84 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getRecipeInterface(recipeInterface) { - return { - signUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.emailPasswordSignUp(input); - }); - }, - signIn: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.emailPasswordSignIn(input); - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty !== undefined) { - // either user is undefined or it's a thirdparty user. - return undefined; - } - return user; - }); - }, - getUserByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.getUsersByEmail(input); - for (let i = 0; i < result.length; i++) { - if (result[i].thirdParty === undefined) { - return result[i]; - } - } - return undefined; - }); - }, - createResetPasswordToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.createResetPasswordToken(input); - }); - }, - resetPasswordUsingToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.resetPasswordUsingToken(input); - }); - }, - updateEmailOrPassword: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.updateEmailOrPassword(input); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.d.ts deleted file mode 100644 index d24f05e67..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "../types"; -import { Querier } from "../../../querier"; -export default function getRecipeInterface(emailPasswordQuerier: Querier, thirdPartyQuerier?: Querier): RecipeInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js deleted file mode 100644 index c66e7bb62..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js +++ /dev/null @@ -1,148 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeImplementation_1 = __importDefault(require("../../emailpassword/recipeImplementation")); -const recipeImplementation_2 = __importDefault(require("../../thirdparty/recipeImplementation")); -const emailPasswordRecipeImplementation_1 = __importDefault(require("./emailPasswordRecipeImplementation")); -const thirdPartyRecipeImplementation_1 = __importDefault(require("./thirdPartyRecipeImplementation")); -function getRecipeInterface(emailPasswordQuerier, thirdPartyQuerier) { - let originalEmailPasswordImplementation = recipeImplementation_1.default(emailPasswordQuerier); - let originalThirdPartyImplementation; - if (thirdPartyQuerier !== undefined) { - originalThirdPartyImplementation = recipeImplementation_2.default(thirdPartyQuerier); - } - return { - emailPasswordSignUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield originalEmailPasswordImplementation.signUp.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); - }, - emailPasswordSignIn: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.signIn.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); - }, - thirdPartySignInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (originalThirdPartyImplementation === undefined) { - throw new Error("No thirdparty provider configured"); - } - return originalThirdPartyImplementation.signInUp.bind(thirdPartyRecipeImplementation_1.default(this))( - input - ); - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield originalEmailPasswordImplementation.getUserById.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - if (user !== undefined) { - return user; - } - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return yield originalThirdPartyImplementation.getUserById.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); - }, - getUsersByEmail: function ({ email, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let userFromEmailPass = yield originalEmailPasswordImplementation.getUserByEmail.bind( - emailPasswordRecipeImplementation_1.default(this) - )({ email, userContext }); - if (originalThirdPartyImplementation === undefined) { - return userFromEmailPass === undefined ? [] : [userFromEmailPass]; - } - let usersFromThirdParty = yield originalThirdPartyImplementation.getUsersByEmail.bind( - thirdPartyRecipeImplementation_1.default(this) - )({ email, userContext }); - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }); - }, - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); - }, - createResetPasswordToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.createResetPasswordToken.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); - }, - resetPasswordUsingToken: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.resetPasswordUsingToken.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); - }, - updateEmailOrPassword: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield this.getUserById({ userId: input.userId, userContext: input.userContext }); - if (user === undefined) { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } else if (user.thirdParty !== undefined) { - throw new Error("Cannot update email or password of a user who signed up using third party login."); - } - return originalEmailPasswordImplementation.updateEmailOrPassword.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.d.ts b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.d.ts deleted file mode 100644 index fc596f02a..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "../../thirdparty/types"; -import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; -export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js deleted file mode 100644 index 3dd764785..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js +++ /dev/null @@ -1,94 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getRecipeInterface(recipeInterface) { - return { - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || user.thirdParty === undefined) { - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - }; - }); - }, - signInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.thirdPartySignInUp(input); - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: { - email: result.user.email, - id: result.user.id, - timeJoined: result.user.timeJoined, - thirdParty: result.user.thirdParty, - }, - }; - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty === undefined) { - // either user is undefined or it's an email password user. - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - }; - }); - }, - getUsersByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let users = yield recipeInterface.getUsersByEmail(input); - // we filter out all non thirdparty users. - return users.filter((u) => { - return u.thirdParty !== undefined; - }); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/thirdpartyemailpassword/types.d.ts b/lib/build/recipe/thirdpartyemailpassword/types.d.ts deleted file mode 100644 index f4bbb9682..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/types.d.ts +++ /dev/null @@ -1,287 +0,0 @@ -// @ts-nocheck -import { TypeProvider, APIOptions as ThirdPartyAPIOptionsOriginal } from "../thirdparty/types"; -import { - NormalisedFormField, - TypeFormField, - TypeInputFormField, - TypeInputResetPasswordUsingTokenFeature, - APIOptions as EmailPasswordAPIOptionsOriginal, - TypeEmailPasswordEmailDeliveryInput, - RecipeInterface as EPRecipeInterface, -} from "../emailpassword/types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse } from "../../types"; -export declare type User = { - id: string; - timeJoined: number; - email: string; - thirdParty?: { - id: string; - userId: string; - }; -}; -export declare type TypeContextEmailPasswordSignUp = { - loginType: "emailpassword"; - formFields: TypeFormField[]; -}; -export declare type TypeContextEmailPasswordSignIn = { - loginType: "emailpassword"; -}; -export declare type TypeContextThirdParty = { - loginType: "thirdparty"; - thirdPartyAuthCodeResponse: any; -}; -export declare type TypeInputSignUp = { - formFields?: TypeInputFormField[]; -}; -export declare type TypeNormalisedInputSignUp = { - formFields: NormalisedFormField[]; -}; -export declare type TypeInput = { - signUpFeature?: TypeInputSignUp; - providers?: TypeProvider[]; - emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - signUpFeature: TypeNormalisedInputSignUp; - providers: TypeProvider[]; - getEmailDeliveryConfig: ( - emailPasswordRecipeImpl: EPRecipeInterface, - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - getUsersByEmail(input: { email: string; userContext: any }): Promise; - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise; - thirdPartySignInUp(input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - emailPasswordSignUp(input: { - email: string; - password: string; - userContext: any; - }): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - >; - emailPasswordSignIn(input: { - email: string; - password: string; - userContext: any; - }): Promise< - | { - status: "OK"; - user: User; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - >; - createResetPasswordToken(input: { - userId: string; - userContext: any; - }): Promise< - | { - status: "OK"; - token: string; - } - | { - status: "UNKNOWN_USER_ID_ERROR"; - } - >; - resetPasswordUsingToken(input: { - token: string; - newPassword: string; - userContext: any; - }): Promise< - | { - status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - >; - updateEmailOrPassword(input: { - userId: string; - email?: string; - password?: string; - userContext: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; -}; -export declare type EmailPasswordAPIOptions = EmailPasswordAPIOptionsOriginal; -export declare type ThirdPartyAPIOptions = ThirdPartyAPIOptionsOriginal; -export declare type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - provider: TypeProvider; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - url: string; - } - | GeneralErrorResponse - >); - emailPasswordEmailExistsGET: - | undefined - | ((input: { - email: string; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); - generatePasswordResetTokenPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - >); - passwordResetPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - token: string; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - userId?: string; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - | GeneralErrorResponse - >); - thirdPartySignInUpPOST: - | undefined - | ((input: { - provider: TypeProvider; - code: string; - redirectURI: string; - authCodeResponse?: any; - clientId?: string; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - authCodeResponse: any; - } - | GeneralErrorResponse - | { - status: "NO_EMAIL_GIVEN_BY_PROVIDER"; - } - >); - emailPasswordSignInPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - | GeneralErrorResponse - >); - emailPasswordSignUpPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | GeneralErrorResponse - >); - appleRedirectHandlerPOST: - | undefined - | ((input: { code: string; state: string; options: ThirdPartyAPIOptions; userContext: any }) => Promise); -}; -export declare type TypeThirdPartyEmailPasswordEmailDeliveryInput = TypeEmailPasswordEmailDeliveryInput; diff --git a/lib/build/recipe/thirdpartyemailpassword/types.js b/lib/build/recipe/thirdpartyemailpassword/types.js deleted file mode 100644 index c8ad2e549..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/types.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/thirdpartyemailpassword/utils.d.ts b/lib/build/recipe/thirdpartyemailpassword/utils.d.ts deleted file mode 100644 index 60b02c8b9..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/utils.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import { TypeInput, TypeNormalisedInput } from "./types"; -import Recipe from "./recipe"; -export declare function validateAndNormaliseUserInput( - recipeInstance: Recipe, - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; diff --git a/lib/build/recipe/thirdpartyemailpassword/utils.js b/lib/build/recipe/thirdpartyemailpassword/utils.js deleted file mode 100644 index daa8a505c..000000000 --- a/lib/build/recipe/thirdpartyemailpassword/utils.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; -const utils_1 = require("../emailpassword/utils"); -const backwardCompatibility_1 = __importDefault(require("./emaildelivery/services/backwardCompatibility")); -function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { - let signUpFeature = validateAndNormaliseSignUpConfig( - recipeInstance, - appInfo, - config === undefined ? undefined : config.signUpFeature - ); - let resetPasswordUsingTokenFeature = config === undefined ? undefined : config.resetPasswordUsingTokenFeature; - let providers = config === undefined || config.providers === undefined ? [] : config.providers; - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - function getEmailDeliveryConfig(emailPasswordRecipeImpl, isInServerlessEnv) { - var _a; - let emailService = - (_a = config === null || config === void 0 ? void 0 : config.emailDelivery) === null || _a === void 0 - ? void 0 - : _a.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation - */ - if (emailService === undefined) { - emailService = new backwardCompatibility_1.default( - emailPasswordRecipeImpl, - appInfo, - isInServerlessEnv, - config === null || config === void 0 ? void 0 : config.resetPasswordUsingTokenFeature - ); - } - return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }); - } - return { - override, - getEmailDeliveryConfig, - signUpFeature, - providers, - resetPasswordUsingTokenFeature, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; -function validateAndNormaliseSignUpConfig(_, __, config) { - let formFields = utils_1.normaliseSignUpFormFields(config === undefined ? undefined : config.formFields); - return { - formFields, - }; -} diff --git a/lib/build/recipe/thirdpartypasswordless/api/implementation.d.ts b/lib/build/recipe/thirdpartypasswordless/api/implementation.d.ts deleted file mode 100644 index 0218549fa..000000000 --- a/lib/build/recipe/thirdpartypasswordless/api/implementation.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../types"; -export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/api/implementation.js b/lib/build/recipe/thirdpartypasswordless/api/implementation.js deleted file mode 100644 index cb792ceb7..000000000 --- a/lib/build/recipe/thirdpartypasswordless/api/implementation.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const implementation_1 = __importDefault(require("../../passwordless/api/implementation")); -const implementation_2 = __importDefault(require("../../thirdparty/api/implementation")); -const passwordlessAPIImplementation_1 = __importDefault(require("./passwordlessAPIImplementation")); -const thirdPartyAPIImplementation_1 = __importDefault(require("./thirdPartyAPIImplementation")); -function getAPIImplementation() { - var _a, _b, _c, _d, _e, _f, _g, _h; - let passwordlessImplementation = implementation_1.default(); - let thirdPartyImplementation = implementation_2.default(); - return { - consumeCodePOST: - (_a = passwordlessImplementation.consumeCodePOST) === null || _a === void 0 - ? void 0 - : _a.bind(passwordlessAPIImplementation_1.default(this)), - createCodePOST: - (_b = passwordlessImplementation.createCodePOST) === null || _b === void 0 - ? void 0 - : _b.bind(passwordlessAPIImplementation_1.default(this)), - passwordlessUserEmailExistsGET: - (_c = passwordlessImplementation.emailExistsGET) === null || _c === void 0 - ? void 0 - : _c.bind(passwordlessAPIImplementation_1.default(this)), - passwordlessUserPhoneNumberExistsGET: - (_d = passwordlessImplementation.phoneNumberExistsGET) === null || _d === void 0 - ? void 0 - : _d.bind(passwordlessAPIImplementation_1.default(this)), - resendCodePOST: - (_e = passwordlessImplementation.resendCodePOST) === null || _e === void 0 - ? void 0 - : _e.bind(passwordlessAPIImplementation_1.default(this)), - authorisationUrlGET: - (_f = thirdPartyImplementation.authorisationUrlGET) === null || _f === void 0 - ? void 0 - : _f.bind(thirdPartyAPIImplementation_1.default(this)), - thirdPartySignInUpPOST: - (_g = thirdPartyImplementation.signInUpPOST) === null || _g === void 0 - ? void 0 - : _g.bind(thirdPartyAPIImplementation_1.default(this)), - appleRedirectHandlerPOST: - (_h = thirdPartyImplementation.appleRedirectHandlerPOST) === null || _h === void 0 - ? void 0 - : _h.bind(thirdPartyAPIImplementation_1.default(this)), - }; -} -exports.default = getAPIImplementation; diff --git a/lib/build/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.d.ts b/lib/build/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.d.ts deleted file mode 100644 index e37fd1b22..000000000 --- a/lib/build/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../../passwordless"; -import { APIInterface as ThirdPartyPasswordlessAPIInterface } from "../types"; -export default function getIterfaceImpl(apiImplmentation: ThirdPartyPasswordlessAPIInterface): APIInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.js b/lib/build/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.js deleted file mode 100644 index 25e56c8b2..000000000 --- a/lib/build/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -function getIterfaceImpl(apiImplmentation) { - var _a, _b, _c, _d, _e; - return { - emailExistsGET: - (_a = apiImplmentation.passwordlessUserEmailExistsGET) === null || _a === void 0 - ? void 0 - : _a.bind(apiImplmentation), - consumeCodePOST: - (_b = apiImplmentation.consumeCodePOST) === null || _b === void 0 ? void 0 : _b.bind(apiImplmentation), - createCodePOST: - (_c = apiImplmentation.createCodePOST) === null || _c === void 0 ? void 0 : _c.bind(apiImplmentation), - phoneNumberExistsGET: - (_d = apiImplmentation.passwordlessUserPhoneNumberExistsGET) === null || _d === void 0 - ? void 0 - : _d.bind(apiImplmentation), - resendCodePOST: - (_e = apiImplmentation.resendCodePOST) === null || _e === void 0 ? void 0 : _e.bind(apiImplmentation), - }; -} -exports.default = getIterfaceImpl; diff --git a/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.d.ts b/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.d.ts deleted file mode 100644 index 11fc459c9..000000000 --- a/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { APIInterface } from "../../thirdparty"; -import { APIInterface as ThirdPartyPasswordlessAPIInterface } from "../types"; -export default function getIterfaceImpl(apiImplmentation: ThirdPartyPasswordlessAPIInterface): APIInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.js b/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.js deleted file mode 100644 index e3d01c889..000000000 --- a/lib/build/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getIterfaceImpl(apiImplmentation) { - var _a, _b, _c; - const signInUpPOSTFromThirdPartyPasswordless = - (_a = apiImplmentation.thirdPartySignInUpPOST) === null || _a === void 0 ? void 0 : _a.bind(apiImplmentation); - return { - authorisationUrlGET: - (_b = apiImplmentation.authorisationUrlGET) === null || _b === void 0 ? void 0 : _b.bind(apiImplmentation), - appleRedirectHandlerPOST: - (_c = apiImplmentation.appleRedirectHandlerPOST) === null || _c === void 0 - ? void 0 - : _c.bind(apiImplmentation), - signInUpPOST: - signInUpPOSTFromThirdPartyPasswordless === undefined - ? undefined - : function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield signInUpPOSTFromThirdPartyPasswordless(input); - if (result.status === "OK") { - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return Object.assign(Object.assign({}, result), { - user: Object.assign(Object.assign({}, result.user), { - thirdParty: Object.assign({}, result.user.thirdParty), - }), - }); - } - return result; - }); - }, - }; -} -exports.default = getIterfaceImpl; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.d.ts deleted file mode 100644 index 3e90372c3..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private passwordlessBackwardCompatibilityService; - constructor( - appInfo: NormalisedAppinfo, - passwordlessFeature?: { - createAndSendCustomEmail?: ( - input: { - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - ); - sendEmail: ( - input: TypeThirdPartyPasswordlessEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.js deleted file mode 100644 index d61010ab5..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const backwardCompatibility_1 = __importDefault( - require("../../../../passwordless/emaildelivery/services/backwardCompatibility") -); -class BackwardCompatibilityService { - constructor(appInfo, passwordlessFeature) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessBackwardCompatibilityService.sendEmail(input); - }); - { - this.passwordlessBackwardCompatibilityService = new backwardCompatibility_1.default( - appInfo, - passwordlessFeature === null || passwordlessFeature === void 0 - ? void 0 - : passwordlessFeature.createAndSendCustomEmail - ); - } - } -} -exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/index.d.ts b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/index.d.ts deleted file mode 100644 index 4de04d983..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -import SMTP from "./smtp"; -export declare let SMTPService: typeof SMTP; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/index.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/index.js deleted file mode 100644 index 7e07f6706..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/index.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SMTPService = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const smtp_1 = __importDefault(require("./smtp")); -exports.SMTPService = smtp_1.default; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.d.ts deleted file mode 100644 index ebb0eb32a..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - private passwordlessSMTPService; - constructor(config: TypeInput); - sendEmail: ( - input: TypeThirdPartyPasswordlessEmailDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.js deleted file mode 100644 index 88e970f1c..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const nodemailer_1 = require("nodemailer"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const serviceImplementation_1 = require("./serviceImplementation"); -const smtp_1 = __importDefault(require("../../../../passwordless/emaildelivery/services/smtp")); -const passwordlessServiceImplementation_1 = __importDefault( - require("./serviceImplementation/passwordlessServiceImplementation") -); -class SMTPService { - constructor(config) { - this.sendEmail = (input) => - __awaiter(this, void 0, void 0, function* () { - return yield this.passwordlessSMTPService.sendEmail(input); - }); - const transporter = nodemailer_1.createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new supertokens_js_override_1.default( - serviceImplementation_1.getServiceImplementation(transporter, config.smtpSettings.from) - ); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - this.passwordlessSMTPService = new smtp_1.default({ - smtpSettings: config.smtpSettings, - override: (_) => { - return passwordlessServiceImplementation_1.default(this.serviceImpl); - }, - }); - } -} -exports.default = SMTPService; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.d.ts b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.d.ts deleted file mode 100644 index 87aab5100..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../../types"; -import { Transporter } from "nodemailer"; -import { ServiceInterface } from "../../../../../../ingredients/emaildelivery/services/smtp"; -export declare function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.js deleted file mode 100644 index 61a874879..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getServiceImplementation = void 0; -const serviceImplementation_1 = require("../../../../../passwordless/emaildelivery/services/smtp/serviceImplementation"); -const passwordlessServiceImplementation_1 = __importDefault(require("./passwordlessServiceImplementation")); -function getServiceImplementation(transporter, from) { - let passwordlessServiceImpl = serviceImplementation_1.getServiceImplementation(transporter, from); - return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - yield transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - }); - }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield passwordlessServiceImpl.getContent.bind(passwordlessServiceImplementation_1.default(this))( - input - ); - }); - }, - }; -} -exports.getServiceImplementation = getServiceImplementation; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.d.ts b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.d.ts deleted file mode 100644 index 0a0cece68..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../../types"; -import { ServiceInterface } from "../../../../../../ingredients/emaildelivery/services/smtp"; -import { TypePasswordlessEmailDeliveryInput } from "../../../../../passwordless/types"; -export default function getServiceInterface( - thirdpartyPasswordlessServiceImplementation: ServiceInterface -): ServiceInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.js b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.js deleted file mode 100644 index 9da544281..000000000 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getServiceInterface(thirdpartyPasswordlessServiceImplementation) { - return { - sendRawEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return thirdpartyPasswordlessServiceImplementation.sendRawEmail(input); - }); - }, - getContent: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield thirdpartyPasswordlessServiceImplementation.getContent(input); - }); - }, - }; -} -exports.default = getServiceInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/error.d.ts b/lib/build/recipe/thirdpartypasswordless/error.d.ts deleted file mode 100644 index 1d9c33665..000000000 --- a/lib/build/recipe/thirdpartypasswordless/error.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -export default class ThirdPartyEmailPasswordError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }); -} diff --git a/lib/build/recipe/thirdpartypasswordless/error.js b/lib/build/recipe/thirdpartypasswordless/error.js deleted file mode 100644 index 0114fec0e..000000000 --- a/lib/build/recipe/thirdpartypasswordless/error.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class ThirdPartyEmailPasswordError extends error_1.default { - constructor(options) { - super(Object.assign({}, options)); - this.fromRecipe = "thirdpartypasswordless"; - } -} -exports.default = ThirdPartyEmailPasswordError; diff --git a/lib/build/recipe/thirdpartypasswordless/index.d.ts b/lib/build/recipe/thirdpartypasswordless/index.d.ts deleted file mode 100644 index 0e8533e84..000000000 --- a/lib/build/recipe/thirdpartypasswordless/index.d.ts +++ /dev/null @@ -1,221 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { - RecipeInterface, - User, - APIInterface, - PasswordlessAPIOptions, - ThirdPartyAPIOptions, - TypeThirdPartyPasswordlessEmailDeliveryInput, -} from "./types"; -import { TypeProvider } from "../thirdparty/types"; -import { TypePasswordlessSmsDeliveryInput } from "../passwordless/types"; -export default class Wrapper { - static init: typeof Recipe.init; - static Error: typeof SuperTokensError; - static thirdPartySignInUp( - thirdPartyId: string, - thirdPartyUserId: string, - email: string, - userContext?: any - ): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - static getUserByThirdPartyInfo( - thirdPartyId: string, - thirdPartyUserId: string, - userContext?: any - ): Promise; - static getUserById(userId: string, userContext?: any): Promise; - static getUsersByEmail(email: string, userContext?: any): Promise; - static createCode( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { - userInputCode?: string; - userContext?: any; - } - ): Promise<{ - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - }>; - static createNewCodeForDevice(input: { - deviceId: string; - userInputCode?: string; - userContext?: any; - }): Promise< - | { - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - } - | { - status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR"; - } - >; - static consumeCode( - input: - | { - preAuthSessionId: string; - userInputCode: string; - deviceId: string; - userContext?: any; - } - | { - preAuthSessionId: string; - linkCode: string; - userContext?: any; - } - ): Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | { - status: "RESTART_FLOW_ERROR"; - } - >; - static getUserByPhoneNumber(input: { phoneNumber: string; userContext?: any }): Promise; - static updatePasswordlessUser(input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext?: any; - }): Promise<{ - status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; - static revokeAllCodes( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ): Promise<{ - status: "OK"; - }>; - static revokeCode(input: { - codeId: string; - userContext?: any; - }): Promise<{ - status: "OK"; - }>; - static listCodesByEmail(input: { - email: string; - userContext?: any; - }): Promise; - static listCodesByPhoneNumber(input: { - phoneNumber: string; - userContext?: any; - }): Promise; - static listCodesByDeviceId(input: { - deviceId: string; - userContext?: any; - }): Promise; - static listCodesByPreAuthSessionId(input: { - preAuthSessionId: string; - userContext?: any; - }): Promise; - static createMagicLink( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ): Promise; - static passwordlessSignInUp( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ): Promise<{ - status: string; - createdNewUser: boolean; - user: import("../passwordless/types").User; - }>; - static Google: typeof import("../thirdparty/providers/google").default; - static Github: typeof import("../thirdparty/providers/github").default; - static Facebook: typeof import("../thirdparty/providers/facebook").default; - static Apple: typeof import("../thirdparty/providers/apple").default; - static Discord: typeof import("../thirdparty/providers/discord").default; - static GoogleWorkspaces: typeof import("../thirdparty/providers/googleWorkspaces").default; - static Bitbucket: typeof import("../thirdparty/providers/bitbucket").default; - static GitLab: typeof import("../thirdparty/providers/gitlab").default; - static sendEmail( - input: TypeThirdPartyPasswordlessEmailDeliveryInput & { - userContext?: any; - } - ): Promise; - static sendSms( - input: TypePasswordlessSmsDeliveryInput & { - userContext?: any; - } - ): Promise; -} -export declare let init: typeof Recipe.init; -export declare let Error: typeof SuperTokensError; -export declare let thirdPartySignInUp: typeof Wrapper.thirdPartySignInUp; -export declare let passwordlessSignInUp: typeof Wrapper.passwordlessSignInUp; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByThirdPartyInfo: typeof Wrapper.getUserByThirdPartyInfo; -export declare let getUsersByEmail: typeof Wrapper.getUsersByEmail; -export declare let createCode: typeof Wrapper.createCode; -export declare let consumeCode: typeof Wrapper.consumeCode; -export declare let getUserByPhoneNumber: typeof Wrapper.getUserByPhoneNumber; -export declare let listCodesByDeviceId: typeof Wrapper.listCodesByDeviceId; -export declare let listCodesByEmail: typeof Wrapper.listCodesByEmail; -export declare let listCodesByPhoneNumber: typeof Wrapper.listCodesByPhoneNumber; -export declare let listCodesByPreAuthSessionId: typeof Wrapper.listCodesByPreAuthSessionId; -export declare let createNewCodeForDevice: typeof Wrapper.createNewCodeForDevice; -export declare let updatePasswordlessUser: typeof Wrapper.updatePasswordlessUser; -export declare let revokeAllCodes: typeof Wrapper.revokeAllCodes; -export declare let revokeCode: typeof Wrapper.revokeCode; -export declare let createMagicLink: typeof Wrapper.createMagicLink; -export declare let Google: typeof import("../thirdparty/providers/google").default; -export declare let Github: typeof import("../thirdparty/providers/github").default; -export declare let Facebook: typeof import("../thirdparty/providers/facebook").default; -export declare let Apple: typeof import("../thirdparty/providers/apple").default; -export declare let Discord: typeof import("../thirdparty/providers/discord").default; -export declare let GoogleWorkspaces: typeof import("../thirdparty/providers/googleWorkspaces").default; -export declare let Bitbucket: typeof import("../thirdparty/providers/bitbucket").default; -export declare let GitLab: typeof import("../thirdparty/providers/gitlab").default; -export type { RecipeInterface, TypeProvider, User, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions }; -export declare let sendEmail: typeof Wrapper.sendEmail; -export declare let sendSms: typeof Wrapper.sendSms; diff --git a/lib/build/recipe/thirdpartypasswordless/index.js b/lib/build/recipe/thirdpartypasswordless/index.js deleted file mode 100644 index fd5d6fd5a..000000000 --- a/lib/build/recipe/thirdpartypasswordless/index.js +++ /dev/null @@ -1,236 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendSms = exports.sendEmail = exports.GitLab = exports.Bitbucket = exports.GoogleWorkspaces = exports.Discord = exports.Apple = exports.Facebook = exports.Github = exports.Google = exports.createMagicLink = exports.revokeCode = exports.revokeAllCodes = exports.updatePasswordlessUser = exports.createNewCodeForDevice = exports.listCodesByPreAuthSessionId = exports.listCodesByPhoneNumber = exports.listCodesByEmail = exports.listCodesByDeviceId = exports.getUserByPhoneNumber = exports.consumeCode = exports.createCode = exports.getUsersByEmail = exports.getUserByThirdPartyInfo = exports.getUserById = exports.passwordlessSignInUp = exports.thirdPartySignInUp = exports.Error = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const error_1 = __importDefault(require("./error")); -const thirdPartyProviders = __importStar(require("../thirdparty/providers")); -class Wrapper { - static thirdPartySignInUp(thirdPartyId, thirdPartyUserId, email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartySignInUp({ - thirdPartyId, - thirdPartyUserId, - email, - userContext, - }); - } - static getUserByThirdPartyInfo(thirdPartyId, thirdPartyUserId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - userContext, - }); - } - static getUserById(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - static getUsersByEmail(email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } - static createCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createCode(Object.assign({ userContext: {} }, input)); - } - static createNewCodeForDevice(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createNewCodeForDevice(Object.assign({ userContext: {} }, input)); - } - static consumeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.consumeCode(Object.assign({ userContext: {} }, input)); - } - static getUserByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.getUserByPhoneNumber(Object.assign({ userContext: {} }, input)); - } - static updatePasswordlessUser(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.updatePasswordlessUser(Object.assign({ userContext: {} }, input)); - } - static revokeAllCodes(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeAllCodes(Object.assign({ userContext: {} }, input)); - } - static revokeCode(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.revokeCode(Object.assign({ userContext: {} }, input)); - } - static listCodesByEmail(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByEmail(Object.assign({ userContext: {} }, input)); - } - static listCodesByPhoneNumber(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPhoneNumber(Object.assign({ userContext: {} }, input)); - } - static listCodesByDeviceId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByDeviceId(Object.assign({ userContext: {} }, input)); - } - static listCodesByPreAuthSessionId(input) { - return recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.listCodesByPreAuthSessionId(Object.assign({ userContext: {} }, input)); - } - static createMagicLink(input) { - return recipe_1.default - .getInstanceOrThrowError() - .passwordlessRecipe.createMagicLink(Object.assign({ userContext: {} }, input)); - } - static passwordlessSignInUp(input) { - return recipe_1.default - .getInstanceOrThrowError() - .passwordlessRecipe.signInUp(Object.assign({ userContext: {} }, input)); - } - // static Okta = thirdPartyProviders.Okta; - // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; - static sendEmail(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .emailDelivery.ingredientInterfaceImpl.sendEmail(Object.assign({ userContext: {} }, input)); - }); - } - static sendSms(input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default - .getInstanceOrThrowError() - .smsDelivery.ingredientInterfaceImpl.sendSms(Object.assign({ userContext: {} }, input)); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -Wrapper.Error = error_1.default; -Wrapper.Google = thirdPartyProviders.Google; -Wrapper.Github = thirdPartyProviders.Github; -Wrapper.Facebook = thirdPartyProviders.Facebook; -Wrapper.Apple = thirdPartyProviders.Apple; -Wrapper.Discord = thirdPartyProviders.Discord; -Wrapper.GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces; -Wrapper.Bitbucket = thirdPartyProviders.Bitbucket; -Wrapper.GitLab = thirdPartyProviders.GitLab; -exports.init = Wrapper.init; -exports.Error = Wrapper.Error; -exports.thirdPartySignInUp = Wrapper.thirdPartySignInUp; -exports.passwordlessSignInUp = Wrapper.passwordlessSignInUp; -exports.getUserById = Wrapper.getUserById; -exports.getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; -exports.getUsersByEmail = Wrapper.getUsersByEmail; -exports.createCode = Wrapper.createCode; -exports.consumeCode = Wrapper.consumeCode; -exports.getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; -exports.listCodesByDeviceId = Wrapper.listCodesByDeviceId; -exports.listCodesByEmail = Wrapper.listCodesByEmail; -exports.listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber; -exports.listCodesByPreAuthSessionId = Wrapper.listCodesByPreAuthSessionId; -exports.createNewCodeForDevice = Wrapper.createNewCodeForDevice; -exports.updatePasswordlessUser = Wrapper.updatePasswordlessUser; -exports.revokeAllCodes = Wrapper.revokeAllCodes; -exports.revokeCode = Wrapper.revokeCode; -exports.createMagicLink = Wrapper.createMagicLink; -exports.Google = Wrapper.Google; -exports.Github = Wrapper.Github; -exports.Facebook = Wrapper.Facebook; -exports.Apple = Wrapper.Apple; -exports.Discord = Wrapper.Discord; -exports.GoogleWorkspaces = Wrapper.GoogleWorkspaces; -exports.Bitbucket = Wrapper.Bitbucket; -exports.GitLab = Wrapper.GitLab; -exports.sendEmail = Wrapper.sendEmail; -exports.sendSms = Wrapper.sendSms; diff --git a/lib/build/recipe/thirdpartypasswordless/recipe.d.ts b/lib/build/recipe/thirdpartypasswordless/recipe.d.ts deleted file mode 100644 index 16351e8c5..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipe.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -// @ts-nocheck -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import PasswordlessRecipe from "../passwordless/recipe"; -import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; -import STError from "./error"; -import { - TypeInput, - TypeNormalisedInput, - RecipeInterface, - APIInterface, - TypeThirdPartyPasswordlessEmailDeliveryInput, - TypeThirdPartyPasswordlessSmsDeliveryInput, -} from "./types"; -import STErrorPasswordless from "../passwordless/error"; -import STErrorThirdParty from "../thirdparty/error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -export default class Recipe extends RecipeModule { - private static instance; - static RECIPE_ID: string; - config: TypeNormalisedInput; - passwordlessRecipe: PasswordlessRecipe; - private thirdPartyRecipe; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - emailDelivery: EmailDeliveryIngredient; - smsDelivery: SmsDeliveryIngredient; - isInServerlessEnv: boolean; - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - recipes: { - thirdPartyInstance: ThirdPartyRecipe | undefined; - passwordlessInstance: PasswordlessRecipe | undefined; - }, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - smsDelivery: SmsDeliveryIngredient | undefined; - } - ); - static init(config: TypeInput): RecipeListFunction; - static reset(): void; - static getInstanceOrThrowError(): Recipe; - getAPIsHandled: () => APIHandled[]; - handleAPIRequest: ( - id: string, - req: BaseRequest, - res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ) => Promise; - handleError: ( - err: STErrorPasswordless | STErrorThirdParty, - request: BaseRequest, - response: BaseResponse - ) => Promise; - getAllCORSHeaders: () => string[]; - isErrorFromThisRecipe: (err: any) => err is STError; -} diff --git a/lib/build/recipe/thirdpartypasswordless/recipe.js b/lib/build/recipe/thirdpartypasswordless/recipe.js deleted file mode 100644 index 4bd2883ad..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipe.js +++ /dev/null @@ -1,241 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const recipe_1 = __importDefault(require("../passwordless/recipe")); -const recipe_2 = __importDefault(require("../thirdparty/recipe")); -const error_1 = __importDefault(require("./error")); -const utils_1 = require("./utils"); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const passwordlessRecipeImplementation_1 = __importDefault( - require("./recipeImplementation/passwordlessRecipeImplementation") -); -const thirdPartyRecipeImplementation_1 = __importDefault( - require("./recipeImplementation/thirdPartyRecipeImplementation") -); -const thirdPartyAPIImplementation_1 = __importDefault(require("./api/thirdPartyAPIImplementation")); -const passwordlessAPIImplementation_1 = __importDefault(require("./api/passwordlessAPIImplementation")); -const implementation_1 = __importDefault(require("./api/implementation")); -const querier_1 = require("../../querier"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); -const smsdelivery_1 = __importDefault(require("../../ingredients/smsdelivery")); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config, recipes, ingredients) { - super(recipeId, appInfo); - this.getAPIsHandled = () => { - let apisHandled = [...this.passwordlessRecipe.getAPIsHandled()]; - if (this.thirdPartyRecipe !== undefined) { - apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()); - } - return apisHandled; - }; - this.handleAPIRequest = (id, req, res, path, method) => - __awaiter(this, void 0, void 0, function* () { - if (this.passwordlessRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined) { - return yield this.passwordlessRecipe.handleAPIRequest(id, req, res, path, method); - } - if ( - this.thirdPartyRecipe !== undefined && - this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined - ) { - return yield this.thirdPartyRecipe.handleAPIRequest(id, req, res, path, method); - } - return false; - }); - this.handleError = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - if (err.fromRecipe === Recipe.RECIPE_ID) { - throw err; - } else { - if (this.passwordlessRecipe.isErrorFromThisRecipe(err)) { - return yield this.passwordlessRecipe.handleError(err, request, response); - } else if ( - this.thirdPartyRecipe !== undefined && - this.thirdPartyRecipe.isErrorFromThisRecipe(err) - ) { - return yield this.thirdPartyRecipe.handleError(err, request, response); - } - throw err; - } - }); - this.getAllCORSHeaders = () => { - let corsHeaders = [...this.passwordlessRecipe.getAllCORSHeaders()]; - if (this.thirdPartyRecipe !== undefined) { - corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()); - } - return corsHeaders; - }; - this.isErrorFromThisRecipe = (err) => { - return ( - error_1.default.isErrorFromSuperTokens(err) && - (err.fromRecipe === Recipe.RECIPE_ID || - this.passwordlessRecipe.isErrorFromThisRecipe(err) || - (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err))) - ); - }; - this.isInServerlessEnv = isInServerlessEnv; - this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default( - querier_1.Querier.getNewInstanceOrThrowError(recipe_1.default.RECIPE_ID), - querier_1.Querier.getNewInstanceOrThrowError(recipe_2.default.RECIPE_ID) - ) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new supertokens_js_override_1.default(implementation_1.default()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new emaildelivery_1.default( - this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv) - ) - : ingredients.emailDelivery; - this.smsDelivery = - ingredients.smsDelivery === undefined - ? new smsdelivery_1.default(this.config.getSmsDeliveryConfig()) - : ingredients.smsDelivery; - this.passwordlessRecipe = - recipes.passwordlessInstance !== undefined - ? recipes.passwordlessInstance - : new recipe_1.default( - recipeId, - appInfo, - isInServerlessEnv, - Object.assign(Object.assign({}, this.config), { - override: { - functions: (_) => { - return passwordlessRecipeImplementation_1.default(this.recipeInterfaceImpl); - }, - apis: (_) => { - return passwordlessAPIImplementation_1.default(this.apiImpl); - }, - }, - }), - { - emailDelivery: this.emailDelivery, - smsDelivery: this.smsDelivery, - } - ); - if (this.config.providers.length !== 0) { - this.thirdPartyRecipe = - recipes.thirdPartyInstance !== undefined - ? recipes.thirdPartyInstance - : new recipe_2.default( - recipeId, - appInfo, - isInServerlessEnv, - { - override: { - functions: (_) => { - return thirdPartyRecipeImplementation_1.default(this.recipeInterfaceImpl); - }, - apis: (_) => { - return thirdPartyAPIImplementation_1.default(this.apiImpl); - }, - }, - signInAndUpFeature: { - providers: this.config.providers, - }, - }, - {}, - { - emailDelivery: this.emailDelivery, - } - ); - } - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe( - Recipe.RECIPE_ID, - appInfo, - isInServerlessEnv, - config, - { - passwordlessInstance: undefined, - thirdPartyInstance: undefined, - }, - { - emailDelivery: undefined, - smsDelivery: undefined, - } - ); - return Recipe.instance; - } else { - throw new Error( - "ThirdPartyPasswordless recipe has already been initialised. Please check your code for bugs." - ); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } -} -exports.default = Recipe; -Recipe.instance = undefined; -Recipe.RECIPE_ID = "thirdpartypasswordless"; diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.d.ts b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.d.ts deleted file mode 100644 index d2d6c2bc2..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "../types"; -import { Querier } from "../../../querier"; -export default function getRecipeInterface(passwordlessQuerier: Querier, thirdPartyQuerier?: Querier): RecipeInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.js b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.js deleted file mode 100644 index 0acca2fd6..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/index.js +++ /dev/null @@ -1,192 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const recipeImplementation_1 = __importDefault(require("../../passwordless/recipeImplementation")); -const recipeImplementation_2 = __importDefault(require("../../thirdparty/recipeImplementation")); -const passwordlessRecipeImplementation_1 = __importDefault(require("./passwordlessRecipeImplementation")); -const thirdPartyRecipeImplementation_1 = __importDefault(require("./thirdPartyRecipeImplementation")); -function getRecipeInterface(passwordlessQuerier, thirdPartyQuerier) { - let originalPasswordlessImplementation = recipeImplementation_1.default(passwordlessQuerier); - let originalThirdPartyImplementation; - if (thirdPartyQuerier !== undefined) { - originalThirdPartyImplementation = recipeImplementation_2.default(thirdPartyQuerier); - } - return { - consumeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.consumeCode.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - createCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.createCode.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - createNewCodeForDevice: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.createNewCodeForDevice.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - getUserByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.getUserByPhoneNumber.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByDeviceId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByDeviceId.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByEmail.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByPhoneNumber.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - listCodesByPreAuthSessionId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.listCodesByPreAuthSessionId.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - revokeAllCodes: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.revokeAllCodes.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - revokeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return originalPasswordlessImplementation.revokeCode.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - updatePasswordlessUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield this.getUserById({ userId: input.userId, userContext: input.userContext }); - if (user === undefined) { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } else if ("thirdParty" in user) { - throw new Error( - "Cannot update passwordless user info for those who signed up using third party login." - ); - } - return originalPasswordlessImplementation.updateUser.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - }); - }, - thirdPartySignInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (originalThirdPartyImplementation === undefined) { - throw new Error("No thirdparty provider configured"); - } - return originalThirdPartyImplementation.signInUp.bind(thirdPartyRecipeImplementation_1.default(this))( - input - ); - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield originalPasswordlessImplementation.getUserById.bind( - passwordlessRecipeImplementation_1.default(this) - )(input); - if (user !== undefined) { - return user; - } - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return yield originalThirdPartyImplementation.getUserById.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); - }, - getUsersByEmail: function ({ email, userContext }) { - return __awaiter(this, void 0, void 0, function* () { - let userFromEmailPass = yield originalPasswordlessImplementation.getUserByEmail.bind( - passwordlessRecipeImplementation_1.default(this) - )({ email, userContext }); - if (originalThirdPartyImplementation === undefined) { - return userFromEmailPass === undefined ? [] : [userFromEmailPass]; - } - let usersFromThirdParty = yield originalThirdPartyImplementation.getUsersByEmail.bind( - thirdPartyRecipeImplementation_1.default(this) - )({ email, userContext }); - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }); - }, - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind( - thirdPartyRecipeImplementation_1.default(this) - )(input); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.d.ts b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.d.ts deleted file mode 100644 index aafe44945..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "../../passwordless/types"; -import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from "../types"; -export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.js b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.js deleted file mode 100644 index 34cc8cf85..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.js +++ /dev/null @@ -1,120 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getRecipeInterface(recipeInterface) { - return { - consumeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.consumeCode(input); - }); - }, - createCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.createCode(input); - }); - }, - createNewCodeForDevice: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.createNewCodeForDevice(input); - }); - }, - getUserByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let users = yield recipeInterface.getUsersByEmail(input); - for (let i = 0; i < users.length; i++) { - let u = users[i]; - if (!("thirdParty" in u)) { - return u; - } - } - return undefined; - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }); - }, - getUserByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserByPhoneNumber(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }); - }, - listCodesByDeviceId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByDeviceId(input); - }); - }, - listCodesByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByEmail(input); - }); - }, - listCodesByPhoneNumber: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByPhoneNumber(input); - }); - }, - listCodesByPreAuthSessionId: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.listCodesByPreAuthSessionId(input); - }); - }, - revokeAllCodes: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.revokeAllCodes(input); - }); - }, - revokeCode: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.revokeCode(input); - }); - }, - updateUser: function (input) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipeInterface.updatePasswordlessUser(input); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.d.ts b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.d.ts deleted file mode 100644 index b93917947..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "../../thirdparty/types"; -import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from "../types"; -export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js deleted file mode 100644 index 715a0171f..000000000 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -Object.defineProperty(exports, "__esModule", { value: true }); -function getRecipeInterface(recipeInterface) { - return { - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || !("thirdParty" in user)) { - return undefined; - } - return user; - }); - }, - signInUp: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.thirdPartySignInUp(input); - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: result.user, - }; - }); - }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || !("thirdParty" in user)) { - // either user is undefined or it's an email password user. - return undefined; - } - return user; - }); - }, - getUsersByEmail: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let users = yield recipeInterface.getUsersByEmail(input); - // we filter out all non thirdparty users. - return users.filter((u) => { - return "thirdParty" in u; - }); - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.d.ts deleted file mode 100644 index 9e1f9745a..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { NormalisedAppinfo } from "../../../../../types"; -export default class BackwardCompatibilityService - implements SmsDeliveryInterface { - private passwordlessBackwardCompatibilityService; - constructor( - appInfo: NormalisedAppinfo, - passwordlessFeature?: { - createAndSendCustomTextMessage?: ( - input: { - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - ); - sendSms: ( - input: TypeThirdPartyPasswordlessSmsDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.js b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.js deleted file mode 100644 index 6fb2a2e25..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const backwardCompatibility_1 = __importDefault( - require("../../../../passwordless/smsdelivery/services/backwardCompatibility") -); -class BackwardCompatibilityService { - constructor(appInfo, passwordlessFeature) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessBackwardCompatibilityService.sendSms(input); - }); - this.passwordlessBackwardCompatibilityService = new backwardCompatibility_1.default( - appInfo, - passwordlessFeature === null || passwordlessFeature === void 0 - ? void 0 - : passwordlessFeature.createAndSendCustomTextMessage - ); - } -} -exports.default = BackwardCompatibilityService; diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/index.d.ts b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/index.d.ts deleted file mode 100644 index f14aacf83..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import Twilio from "./twilio"; -import Supertokens from "./supertokens"; -export declare let TwilioService: typeof Twilio; -export declare let SupertokensService: typeof Supertokens; diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/index.js b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/index.js deleted file mode 100644 index f85fb8900..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/index.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SupertokensService = exports.TwilioService = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const twilio_1 = __importDefault(require("./twilio")); -const supertokens_1 = __importDefault(require("./supertokens")); -exports.TwilioService = twilio_1.default; -exports.SupertokensService = supertokens_1.default; diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.d.ts b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.d.ts deleted file mode 100644 index 2be198af4..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; -export default class SupertokensService implements SmsDeliveryInterface { - private passwordlessSupertokensService; - constructor(apiKey: string); - sendSms: ( - input: TypeThirdPartyPasswordlessSmsDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.js b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.js deleted file mode 100644 index 4c764c79d..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const supertokens_1 = __importDefault(require("../../../../passwordless/smsdelivery/services/supertokens")); -class SupertokensService { - constructor(apiKey) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessSupertokensService.sendSms(input); - }); - this.passwordlessSupertokensService = new supertokens_1.default(apiKey); - } -} -exports.default = SupertokensService; diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.d.ts b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.d.ts deleted file mode 100644 index ec73adb1f..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -import { TypeInput } from "../../../../../ingredients/smsdelivery/services/twilio"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; -export default class TwilioService implements SmsDeliveryInterface { - private passwordlessTwilioService; - constructor(config: TypeInput); - sendSms: ( - input: TypeThirdPartyPasswordlessSmsDeliveryInput & { - userContext: any; - } - ) => Promise; -} diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.js b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.js deleted file mode 100644 index 4dcafed68..000000000 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const index_1 = __importDefault(require("../../../../passwordless/smsdelivery/services/twilio/index")); -class TwilioService { - constructor(config) { - this.sendSms = (input) => - __awaiter(this, void 0, void 0, function* () { - yield this.passwordlessTwilioService.sendSms(input); - }); - this.passwordlessTwilioService = new index_1.default(config); - } -} -exports.default = TwilioService; diff --git a/lib/build/recipe/thirdpartypasswordless/types.d.ts b/lib/build/recipe/thirdpartypasswordless/types.d.ts deleted file mode 100644 index 2f63668b3..000000000 --- a/lib/build/recipe/thirdpartypasswordless/types.d.ts +++ /dev/null @@ -1,418 +0,0 @@ -// @ts-nocheck -import { TypeProvider, APIOptions as ThirdPartyAPIOptionsOriginal } from "../thirdparty/types"; -import { - DeviceType as DeviceTypeOriginal, - APIOptions as PasswordlessAPIOptionsOriginal, - TypePasswordlessEmailDeliveryInput, - TypePasswordlessSmsDeliveryInput, -} from "../passwordless/types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import { - TypeInput as SmsDeliveryTypeInput, - TypeInputWithService as SmsDeliveryTypeInputWithService, -} from "../../ingredients/smsdelivery/types"; -import { GeneralErrorResponse } from "../../types"; -export declare type DeviceType = DeviceTypeOriginal; -export declare type User = ( - | { - email?: string; - phoneNumber?: string; - } - | { - email: string; - thirdParty: { - id: string; - userId: string; - }; - } -) & { - id: string; - timeJoined: number; -}; -export declare type TypeInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } -) & { - /** - * Unlike passwordless recipe, emailDelivery config is outside here because regardless - * of `contactMethod` value, the config is required for email verification recipe - */ - emailDelivery?: EmailDeliveryTypeInput; - smsDelivery?: SmsDeliveryTypeInput; - providers?: TypeProvider[]; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - getCustomUserInputCode?: (userContext: any) => Promise | string; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - } -) & { - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - getCustomUserInputCode?: (userContext: any) => Promise | string; - providers: TypeProvider[]; - getEmailDeliveryConfig: ( - recipeImpl: RecipeInterface, - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - getUsersByEmail(input: { email: string; userContext: any }): Promise; - getUserByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise; - thirdPartySignInUp(input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ - status: "OK"; - createdNewUser: boolean; - user: User; - }>; - createCode: ( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { - userInputCode?: string; - userContext: any; - } - ) => Promise<{ - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - }>; - createNewCodeForDevice: (input: { - deviceId: string; - userInputCode?: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - } - | { - status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR"; - } - >; - consumeCode: ( - input: - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - userContext: any; - } - | { - linkCode: string; - preAuthSessionId: string; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | { - status: "RESTART_FLOW_ERROR"; - } - >; - updatePasswordlessUser: (input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; - revokeAllCodes: ( - input: - | { - email: string; - userContext: any; - } - | { - phoneNumber: string; - userContext: any; - } - ) => Promise<{ - status: "OK"; - }>; - revokeCode: (input: { - codeId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - }>; - listCodesByEmail: (input: { email: string; userContext: any }) => Promise; - listCodesByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - listCodesByDeviceId: (input: { deviceId: string; userContext: any }) => Promise; - listCodesByPreAuthSessionId: (input: { - preAuthSessionId: string; - userContext: any; - }) => Promise; -}; -export declare type PasswordlessAPIOptions = PasswordlessAPIOptionsOriginal; -export declare type ThirdPartyAPIOptions = ThirdPartyAPIOptionsOriginal; -export declare type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - provider: TypeProvider; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - url: string; - } - | GeneralErrorResponse - >); - thirdPartySignInUpPOST: - | undefined - | ((input: { - provider: TypeProvider; - code: string; - redirectURI: string; - authCodeResponse?: any; - clientId?: string; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - authCodeResponse: any; - } - | GeneralErrorResponse - | { - status: "NO_EMAIL_GIVEN_BY_PROVIDER"; - } - >); - appleRedirectHandlerPOST: - | undefined - | ((input: { code: string; state: string; options: ThirdPartyAPIOptions; userContext: any }) => Promise); - createCodePOST: - | undefined - | (( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { - options: PasswordlessAPIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - deviceId: string; - preAuthSessionId: string; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - } - | GeneralErrorResponse - >); - resendCodePOST: - | undefined - | (( - input: { - deviceId: string; - preAuthSessionId: string; - } & { - options: PasswordlessAPIOptions; - userContext: any; - } - ) => Promise< - | GeneralErrorResponse - | { - status: "RESTART_FLOW_ERROR" | "OK"; - } - >); - consumeCodePOST: - | undefined - | (( - input: ( - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - } - | { - linkCode: string; - preAuthSessionId: string; - } - ) & { - options: PasswordlessAPIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | GeneralErrorResponse - | { - status: "RESTART_FLOW_ERROR"; - } - >); - passwordlessUserEmailExistsGET: - | undefined - | ((input: { - email: string; - options: PasswordlessAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); - passwordlessUserPhoneNumberExistsGET: - | undefined - | ((input: { - phoneNumber: string; - options: PasswordlessAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); -}; -export declare type TypeThirdPartyPasswordlessEmailDeliveryInput = TypePasswordlessEmailDeliveryInput; -export declare type TypeThirdPartyPasswordlessSmsDeliveryInput = TypePasswordlessSmsDeliveryInput; diff --git a/lib/build/recipe/thirdpartypasswordless/types.js b/lib/build/recipe/thirdpartypasswordless/types.js deleted file mode 100644 index c8ad2e549..000000000 --- a/lib/build/recipe/thirdpartypasswordless/types.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/thirdpartypasswordless/utils.d.ts b/lib/build/recipe/thirdpartypasswordless/utils.d.ts deleted file mode 100644 index 0fde7f7fd..000000000 --- a/lib/build/recipe/thirdpartypasswordless/utils.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - appInfo: NormalisedAppinfo, - config: TypeInput -): TypeNormalisedInput; diff --git a/lib/build/recipe/thirdpartypasswordless/utils.js b/lib/build/recipe/thirdpartypasswordless/utils.js deleted file mode 100644 index ffdfd159c..000000000 --- a/lib/build/recipe/thirdpartypasswordless/utils.js +++ /dev/null @@ -1,117 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; -const backwardCompatibility_1 = __importDefault(require("./emaildelivery/services/backwardCompatibility")); -const backwardCompatibility_2 = __importDefault(require("./smsdelivery/services/backwardCompatibility")); -function validateAndNormaliseUserInput(appInfo, config) { - let providers = config.providers === undefined ? [] : config.providers; - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - function getEmailDeliveryConfig() { - var _a; - let emailService = - (_a = config === null || config === void 0 ? void 0 : config.emailDelivery) === null || _a === void 0 - ? void 0 - : _a.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation - */ - if (emailService === undefined) { - emailService = new backwardCompatibility_1.default(appInfo, { - createAndSendCustomEmail: - (config === null || config === void 0 ? void 0 : config.contactMethod) !== "PHONE" - ? config === null || config === void 0 - ? void 0 - : config.createAndSendCustomEmail - : undefined, - }); - } - return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }); - } - function getSmsDeliveryConfig() { - var _a; - let smsService = - (_a = config === null || config === void 0 ? void 0 : config.smsDelivery) === null || _a === void 0 - ? void 0 - : _a.service; - /** - * following code is for backward compatibility. - * if user has not passed smsDelivery config, we - * use the createAndSendCustomTextMessage config. If the user - * has not passed even that config, we use the default - * createAndSendCustomTextMessage implementation - */ - if (smsService === undefined) { - smsService = new backwardCompatibility_2.default(appInfo, { - createAndSendCustomTextMessage: - (config === null || config === void 0 ? void 0 : config.contactMethod) !== "EMAIL" - ? config === null || config === void 0 - ? void 0 - : config.createAndSendCustomTextMessage - : undefined, - }); - } - return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.smsDelivery), { - /** - * if we do - * let smsDelivery = { - * service: smsService, - * ...config.smsDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: smsService, - }); - } - return Object.assign(Object.assign({}, config), { - providers, - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - }); -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/usermetadata/index.d.ts b/lib/build/recipe/usermetadata/index.d.ts deleted file mode 100644 index 91f6865fa..000000000 --- a/lib/build/recipe/usermetadata/index.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -// @ts-nocheck -import { JSONObject } from "../../types"; -import Recipe from "./recipe"; -import { RecipeInterface } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static getUserMetadata( - userId: string, - userContext?: any - ): Promise<{ - status: "OK"; - metadata: any; - }>; - static updateUserMetadata( - userId: string, - metadataUpdate: JSONObject, - userContext?: any - ): Promise<{ - status: "OK"; - metadata: JSONObject; - }>; - static clearUserMetadata( - userId: string, - userContext?: any - ): Promise<{ - status: "OK"; - }>; -} -export declare const init: typeof Recipe.init; -export declare const getUserMetadata: typeof Wrapper.getUserMetadata; -export declare const updateUserMetadata: typeof Wrapper.updateUserMetadata; -export declare const clearUserMetadata: typeof Wrapper.clearUserMetadata; -export type { RecipeInterface, JSONObject }; diff --git a/lib/build/recipe/usermetadata/index.js b/lib/build/recipe/usermetadata/index.js deleted file mode 100644 index c5d267836..000000000 --- a/lib/build/recipe/usermetadata/index.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.clearUserMetadata = exports.updateUserMetadata = exports.getUserMetadata = exports.init = void 0; -const recipe_1 = __importDefault(require("./recipe")); -class Wrapper { - static getUserMetadata(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserMetadata({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static updateUserMetadata(userId, metadataUpdate, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateUserMetadata({ - userId, - metadataUpdate, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static clearUserMetadata(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.clearUserMetadata({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -exports.init = Wrapper.init; -exports.getUserMetadata = Wrapper.getUserMetadata; -exports.updateUserMetadata = Wrapper.updateUserMetadata; -exports.clearUserMetadata = Wrapper.clearUserMetadata; diff --git a/lib/build/recipe/usermetadata/recipe.d.ts b/lib/build/recipe/usermetadata/recipe.d.ts deleted file mode 100644 index bbce8012b..000000000 --- a/lib/build/recipe/usermetadata/recipe.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-nocheck -import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import normalisedURLPath from "../../normalisedURLPath"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -export default class Recipe extends RecipeModule { - static RECIPE_ID: string; - private static instance; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - isInServerlessEnv: boolean; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); - static getInstanceOrThrowError(): Recipe; - static init(config?: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled(): APIHandled[]; - handleAPIRequest: ( - _: string, - __: BaseRequest, - ___: BaseResponse, - ____: normalisedURLPath, - _____: HTTPMethod - ) => Promise; - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise; - getAllCORSHeaders(): string[]; - isErrorFromThisRecipe(err: any): err is error; -} diff --git a/lib/build/recipe/usermetadata/recipe.js b/lib/build/recipe/usermetadata/recipe.js deleted file mode 100644 index 1b7ab148b..000000000 --- a/lib/build/recipe/usermetadata/recipe.js +++ /dev/null @@ -1,117 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -const querier_1 = require("../../querier"); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const utils_1 = require("./utils"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config) { - super(recipeId, appInfo); - // This stub is required to implement RecipeModule - this.handleAPIRequest = (_, __, ___, ____, _____) => - __awaiter(this, void 0, void 0, function* () { - throw new Error("Should never come here"); - }); - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - } - /* Init functions */ - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error( - "Initialisation not done. Did you forget to call the UserMetadata.init or SuperTokens.init function?" - ); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("UserMetadata recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - /* RecipeModule functions */ - getAPIsHandled() { - return []; - } - handleError(error, _, __) { - throw error; - } - getAllCORSHeaders() { - return []; - } - isErrorFromThisRecipe(err) { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - } -} -exports.default = Recipe; -Recipe.RECIPE_ID = "usermetadata"; -Recipe.instance = undefined; diff --git a/lib/build/recipe/usermetadata/recipeImplementation.d.ts b/lib/build/recipe/usermetadata/recipeImplementation.d.ts deleted file mode 100644 index 0c838d977..000000000 --- a/lib/build/recipe/usermetadata/recipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "."; -import { Querier } from "../../querier"; -export default function getRecipeInterface(querier: Querier): RecipeInterface; diff --git a/lib/build/recipe/usermetadata/recipeImplementation.js b/lib/build/recipe/usermetadata/recipeImplementation.js deleted file mode 100644 index 1f9653e83..000000000 --- a/lib/build/recipe/usermetadata/recipeImplementation.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier) { - return { - getUserMetadata: function ({ userId }) { - return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user/metadata"), { userId }); - }, - updateUserMetadata: function ({ userId, metadataUpdate }) { - return querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user/metadata"), { - userId, - metadataUpdate, - }); - }, - clearUserMetadata: function ({ userId }) { - return querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/user/metadata/remove"), { - userId, - }); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/usermetadata/types.d.ts b/lib/build/recipe/usermetadata/types.d.ts deleted file mode 100644 index a88b13312..000000000 --- a/lib/build/recipe/usermetadata/types.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -import { JSONObject } from "../../types"; -export declare type TypeInput = { - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type APIInterface = {}; -export declare type RecipeInterface = { - getUserMetadata: (input: { - userId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - metadata: any; - }>; - /** - * Updates the metadata object of the user by doing a shallow merge of the stored and the update JSONs - * and removing properties set to null on the root level of the update object. - * e.g.: - * - stored: `{ "preferences": { "theme":"dark" }, "notifications": { "email": true }, "todos": ["example"] }` - * - update: `{ "notifications": { "sms": true }, "todos": null }` - * - result: `{ "preferences": { "theme":"dark" }, "notifications": { "sms": true } }` - */ - updateUserMetadata: (input: { - userId: string; - metadataUpdate: JSONObject; - userContext: any; - }) => Promise<{ - status: "OK"; - metadata: JSONObject; - }>; - clearUserMetadata: (input: { - userId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - }>; -}; diff --git a/lib/build/recipe/usermetadata/types.js b/lib/build/recipe/usermetadata/types.js deleted file mode 100644 index a7bb3574b..000000000 --- a/lib/build/recipe/usermetadata/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/usermetadata/utils.d.ts b/lib/build/recipe/usermetadata/utils.d.ts deleted file mode 100644 index 4025b1b44..000000000 --- a/lib/build/recipe/usermetadata/utils.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; diff --git a/lib/build/recipe/usermetadata/utils.js b/lib/build/recipe/usermetadata/utils.js deleted file mode 100644 index 74993e81f..000000000 --- a/lib/build/recipe/usermetadata/utils.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; -function validateAndNormaliseUserInput(_, __, config) { - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - return { - override, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/userroles/index.d.ts b/lib/build/recipe/userroles/index.d.ts deleted file mode 100644 index 0e2210593..000000000 --- a/lib/build/recipe/userroles/index.d.ts +++ /dev/null @@ -1,114 +0,0 @@ -// @ts-nocheck -import Recipe from "./recipe"; -import { RecipeInterface } from "./types"; -export default class Wrapper { - static init: typeof Recipe.init; - static PermissionClaim: import("./permissionClaim").PermissionClaimClass; - static UserRoleClaim: import("./userRoleClaim").UserRoleClaimClass; - static addRoleToUser( - userId: string, - role: string, - userContext?: any - ): Promise< - | { - status: "OK"; - didUserAlreadyHaveRole: boolean; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - static removeUserRole( - userId: string, - role: string, - userContext?: any - ): Promise< - | { - status: "OK"; - didUserHaveRole: boolean; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - static getRolesForUser( - userId: string, - userContext?: any - ): Promise<{ - status: "OK"; - roles: string[]; - }>; - static getUsersThatHaveRole( - role: string, - userContext?: any - ): Promise< - | { - status: "OK"; - users: string[]; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - static createNewRoleOrAddPermissions( - role: string, - permissions: string[], - userContext?: any - ): Promise<{ - status: "OK"; - createdNewRole: boolean; - }>; - static getPermissionsForRole( - role: string, - userContext?: any - ): Promise< - | { - status: "OK"; - permissions: string[]; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - static removePermissionsFromRole( - role: string, - permissions: string[], - userContext?: any - ): Promise<{ - status: "OK" | "UNKNOWN_ROLE_ERROR"; - }>; - static getRolesThatHavePermission( - permission: string, - userContext?: any - ): Promise<{ - status: "OK"; - roles: string[]; - }>; - static deleteRole( - role: string, - userContext?: any - ): Promise<{ - status: "OK"; - didRoleExist: boolean; - }>; - static getAllRoles( - userContext?: any - ): Promise<{ - status: "OK"; - roles: string[]; - }>; -} -export declare const init: typeof Recipe.init; -export declare const addRoleToUser: typeof Wrapper.addRoleToUser; -export declare const removeUserRole: typeof Wrapper.removeUserRole; -export declare const getRolesForUser: typeof Wrapper.getRolesForUser; -export declare const getUsersThatHaveRole: typeof Wrapper.getUsersThatHaveRole; -export declare const createNewRoleOrAddPermissions: typeof Wrapper.createNewRoleOrAddPermissions; -export declare const getPermissionsForRole: typeof Wrapper.getPermissionsForRole; -export declare const removePermissionsFromRole: typeof Wrapper.removePermissionsFromRole; -export declare const getRolesThatHavePermission: typeof Wrapper.getRolesThatHavePermission; -export declare const deleteRole: typeof Wrapper.deleteRole; -export declare const getAllRoles: typeof Wrapper.getAllRoles; -export { UserRoleClaim } from "./userRoleClaim"; -export { PermissionClaim } from "./permissionClaim"; -export type { RecipeInterface }; diff --git a/lib/build/recipe/userroles/index.js b/lib/build/recipe/userroles/index.js deleted file mode 100644 index c01687051..000000000 --- a/lib/build/recipe/userroles/index.js +++ /dev/null @@ -1,170 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PermissionClaim = exports.UserRoleClaim = exports.getAllRoles = exports.deleteRole = exports.getRolesThatHavePermission = exports.removePermissionsFromRole = exports.getPermissionsForRole = exports.createNewRoleOrAddPermissions = exports.getUsersThatHaveRole = exports.getRolesForUser = exports.removeUserRole = exports.addRoleToUser = exports.init = void 0; -const permissionClaim_1 = require("./permissionClaim"); -const recipe_1 = __importDefault(require("./recipe")); -const userRoleClaim_1 = require("./userRoleClaim"); -class Wrapper { - static addRoleToUser(userId, role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.addRoleToUser({ - userId, - role, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static removeUserRole(userId, role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removeUserRole({ - userId, - role, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static getRolesForUser(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesForUser({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static getUsersThatHaveRole(role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersThatHaveRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static createNewRoleOrAddPermissions(role, permissions, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewRoleOrAddPermissions({ - role, - permissions, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static getPermissionsForRole(role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getPermissionsForRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static removePermissionsFromRole(role, permissions, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removePermissionsFromRole({ - role, - permissions, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static getRolesThatHavePermission(permission, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesThatHavePermission({ - permission, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static deleteRole(role, userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.deleteRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); - }); - } - static getAllRoles(userContext) { - return __awaiter(this, void 0, void 0, function* () { - return yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getAllRoles({ - userContext: userContext === undefined ? {} : userContext, - }); - }); - } -} -exports.default = Wrapper; -Wrapper.init = recipe_1.default.init; -Wrapper.PermissionClaim = permissionClaim_1.PermissionClaim; -Wrapper.UserRoleClaim = userRoleClaim_1.UserRoleClaim; -exports.init = Wrapper.init; -exports.addRoleToUser = Wrapper.addRoleToUser; -exports.removeUserRole = Wrapper.removeUserRole; -exports.getRolesForUser = Wrapper.getRolesForUser; -exports.getUsersThatHaveRole = Wrapper.getUsersThatHaveRole; -exports.createNewRoleOrAddPermissions = Wrapper.createNewRoleOrAddPermissions; -exports.getPermissionsForRole = Wrapper.getPermissionsForRole; -exports.removePermissionsFromRole = Wrapper.removePermissionsFromRole; -exports.getRolesThatHavePermission = Wrapper.getRolesThatHavePermission; -exports.deleteRole = Wrapper.deleteRole; -exports.getAllRoles = Wrapper.getAllRoles; -var userRoleClaim_2 = require("./userRoleClaim"); -Object.defineProperty(exports, "UserRoleClaim", { - enumerable: true, - get: function () { - return userRoleClaim_2.UserRoleClaim; - }, -}); -var permissionClaim_2 = require("./permissionClaim"); -Object.defineProperty(exports, "PermissionClaim", { - enumerable: true, - get: function () { - return permissionClaim_2.PermissionClaim; - }, -}); diff --git a/lib/build/recipe/userroles/permissionClaim.d.ts b/lib/build/recipe/userroles/permissionClaim.d.ts deleted file mode 100644 index d0c34d159..000000000 --- a/lib/build/recipe/userroles/permissionClaim.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { PrimitiveArrayClaim } from "../session/claimBaseClasses/primitiveArrayClaim"; -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -export declare class PermissionClaimClass extends PrimitiveArrayClaim { - constructor(); -} -export declare const PermissionClaim: PermissionClaimClass; diff --git a/lib/build/recipe/userroles/permissionClaim.js b/lib/build/recipe/userroles/permissionClaim.js deleted file mode 100644 index 8d68c2fb4..000000000 --- a/lib/build/recipe/userroles/permissionClaim.js +++ /dev/null @@ -1,78 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PermissionClaim = exports.PermissionClaimClass = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const primitiveArrayClaim_1 = require("../session/claimBaseClasses/primitiveArrayClaim"); -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -class PermissionClaimClass extends primitiveArrayClaim_1.PrimitiveArrayClaim { - constructor() { - super({ - key: "st-perm", - fetchValue(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipe = recipe_1.default.getInstanceOrThrowError(); - // We fetch the roles because the rolesClaim may not be present in the payload - const userRoles = yield recipe.recipeInterfaceImpl.getRolesForUser({ - userId, - userContext, - }); - // We use a set to filter out duplicates - const userPermissions = new Set(); - for (const role of userRoles.roles) { - const rolePermissions = yield recipe.recipeInterfaceImpl.getPermissionsForRole({ - role, - userContext, - }); - if (rolePermissions.status === "OK") { - for (const perm of rolePermissions.permissions) { - userPermissions.add(perm); - } - } - } - return Array.from(userPermissions); - }); - }, - defaultMaxAgeInSeconds: 300, - }); - } -} -exports.PermissionClaimClass = PermissionClaimClass; -exports.PermissionClaim = new PermissionClaimClass(); diff --git a/lib/build/recipe/userroles/recipe.d.ts b/lib/build/recipe/userroles/recipe.d.ts deleted file mode 100644 index bbce8012b..000000000 --- a/lib/build/recipe/userroles/recipe.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-nocheck -import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import normalisedURLPath from "../../normalisedURLPath"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -export default class Recipe extends RecipeModule { - static RECIPE_ID: string; - private static instance; - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - isInServerlessEnv: boolean; - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); - static getInstanceOrThrowError(): Recipe; - static init(config?: TypeInput): RecipeListFunction; - static reset(): void; - getAPIsHandled(): APIHandled[]; - handleAPIRequest: ( - _: string, - __: BaseRequest, - ___: BaseResponse, - ____: normalisedURLPath, - _____: HTTPMethod - ) => Promise; - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise; - getAllCORSHeaders(): string[]; - isErrorFromThisRecipe(err: any): err is error; -} diff --git a/lib/build/recipe/userroles/recipe.js b/lib/build/recipe/userroles/recipe.js deleted file mode 100644 index 92931f54f..000000000 --- a/lib/build/recipe/userroles/recipe.js +++ /dev/null @@ -1,129 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -const querier_1 = require("../../querier"); -const recipeModule_1 = __importDefault(require("../../recipeModule")); -const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); -const utils_1 = require("./utils"); -const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); -const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); -const recipe_1 = __importDefault(require("../session/recipe")); -const userRoleClaim_1 = require("./userRoleClaim"); -const permissionClaim_1 = require("./permissionClaim"); -class Recipe extends recipeModule_1.default { - constructor(recipeId, appInfo, isInServerlessEnv, config) { - super(recipeId, appInfo); - // This stub is required to implement RecipeModule - this.handleAPIRequest = (_, __, ___, ____, _____) => - __awaiter(this, void 0, void 0, function* () { - throw new Error("Should never come here"); - }); - this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { - if (!this.config.skipAddingRolesToAccessToken) { - recipe_1.default.getInstanceOrThrowError().addClaimFromOtherRecipe(userRoleClaim_1.UserRoleClaim); - } - if (!this.config.skipAddingPermissionsToAccessToken) { - recipe_1.default.getInstanceOrThrowError().addClaimFromOtherRecipe(permissionClaim_1.PermissionClaim); - } - }); - } - /* Init functions */ - static getInstanceOrThrowError() { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error( - "Initialisation not done. Did you forget to call the UserRoles.init or SuperTokens.init functions?" - ); - } - static init(config) { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("UserRoles recipe has already been initialised. Please check your code for bugs."); - } - }; - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - /* RecipeModule functions */ - getAPIsHandled() { - return []; - } - handleError(error, _, __) { - throw error; - } - getAllCORSHeaders() { - return []; - } - isErrorFromThisRecipe(err) { - return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - } -} -exports.default = Recipe; -Recipe.RECIPE_ID = "userroles"; -Recipe.instance = undefined; diff --git a/lib/build/recipe/userroles/recipeImplementation.d.ts b/lib/build/recipe/userroles/recipeImplementation.d.ts deleted file mode 100644 index 86bf78a27..000000000 --- a/lib/build/recipe/userroles/recipeImplementation.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -import { RecipeInterface } from "./types"; -import { Querier } from "../../querier"; -export default function getRecipeInterface(querier: Querier): RecipeInterface; diff --git a/lib/build/recipe/userroles/recipeImplementation.js b/lib/build/recipe/userroles/recipeImplementation.js deleted file mode 100644 index 459e621f1..000000000 --- a/lib/build/recipe/userroles/recipeImplementation.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier) { - return { - addRoleToUser: function ({ userId, role }) { - return querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user/role"), { userId, role }); - }, - removeUserRole: function ({ userId, role }) { - return querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/user/role/remove"), { - userId, - role, - }); - }, - getRolesForUser: function ({ userId }) { - return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user/roles"), { userId }); - }, - getUsersThatHaveRole: function ({ role }) { - return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/role/users"), { role }); - }, - createNewRoleOrAddPermissions: function ({ role, permissions }) { - return querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/role"), { role, permissions }); - }, - getPermissionsForRole: function ({ role }) { - return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/role/permissions"), { role }); - }, - removePermissionsFromRole: function ({ role, permissions }) { - return querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/role/permissions/remove"), { - role, - permissions, - }); - }, - getRolesThatHavePermission: function ({ permission }) { - return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/permission/roles"), { permission }); - }, - deleteRole: function ({ role }) { - return querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/role/remove"), { role }); - }, - getAllRoles: function () { - return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/roles"), {}); - }, - }; -} -exports.default = getRecipeInterface; diff --git a/lib/build/recipe/userroles/types.d.ts b/lib/build/recipe/userroles/types.d.ts deleted file mode 100644 index 9552d9f54..000000000 --- a/lib/build/recipe/userroles/types.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -// @ts-nocheck -import OverrideableBuilder from "supertokens-js-override"; -export declare type TypeInput = { - skipAddingRolesToAccessToken?: boolean; - skipAddingPermissionsToAccessToken?: boolean; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type TypeNormalisedInput = { - skipAddingRolesToAccessToken: boolean; - skipAddingPermissionsToAccessToken: boolean; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; -export declare type APIInterface = {}; -export declare type RecipeInterface = { - addRoleToUser: (input: { - userId: string; - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - didUserAlreadyHaveRole: boolean; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - removeUserRole: (input: { - userId: string; - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - didUserHaveRole: boolean; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - getRolesForUser: (input: { - userId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - roles: string[]; - }>; - getUsersThatHaveRole: (input: { - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - users: string[]; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - createNewRoleOrAddPermissions: (input: { - role: string; - permissions: string[]; - userContext: any; - }) => Promise<{ - status: "OK"; - createdNewRole: boolean; - }>; - getPermissionsForRole: (input: { - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - permissions: string[]; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - removePermissionsFromRole: (input: { - role: string; - permissions: string[]; - userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_ROLE_ERROR"; - }>; - getRolesThatHavePermission: (input: { - permission: string; - userContext: any; - }) => Promise<{ - status: "OK"; - roles: string[]; - }>; - deleteRole: (input: { - role: string; - userContext: any; - }) => Promise<{ - status: "OK"; - didRoleExist: boolean; - }>; - getAllRoles: (input: { - userContext: any; - }) => Promise<{ - status: "OK"; - roles: string[]; - }>; -}; diff --git a/lib/build/recipe/userroles/types.js b/lib/build/recipe/userroles/types.js deleted file mode 100644 index a7bb3574b..000000000 --- a/lib/build/recipe/userroles/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/userroles/userRoleClaim.d.ts b/lib/build/recipe/userroles/userRoleClaim.d.ts deleted file mode 100644 index 75c0635d5..000000000 --- a/lib/build/recipe/userroles/userRoleClaim.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { PrimitiveArrayClaim } from "../session/claimBaseClasses/primitiveArrayClaim"; -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -export declare class UserRoleClaimClass extends PrimitiveArrayClaim { - constructor(); -} -export declare const UserRoleClaim: UserRoleClaimClass; diff --git a/lib/build/recipe/userroles/userRoleClaim.js b/lib/build/recipe/userroles/userRoleClaim.js deleted file mode 100644 index 9f0a777aa..000000000 --- a/lib/build/recipe/userroles/userRoleClaim.js +++ /dev/null @@ -1,64 +0,0 @@ -"use strict"; -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.UserRoleClaim = exports.UserRoleClaimClass = void 0; -const recipe_1 = __importDefault(require("./recipe")); -const primitiveArrayClaim_1 = require("../session/claimBaseClasses/primitiveArrayClaim"); -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -class UserRoleClaimClass extends primitiveArrayClaim_1.PrimitiveArrayClaim { - constructor() { - super({ - key: "st-role", - fetchValue(userId, userContext) { - return __awaiter(this, void 0, void 0, function* () { - const recipe = recipe_1.default.getInstanceOrThrowError(); - const res = yield recipe.recipeInterfaceImpl.getRolesForUser({ - userId, - userContext, - }); - return res.roles; - }); - }, - defaultMaxAgeInSeconds: 300, - }); - } -} -exports.UserRoleClaimClass = UserRoleClaimClass; -exports.UserRoleClaim = new UserRoleClaimClass(); diff --git a/lib/build/recipe/userroles/utils.d.ts b/lib/build/recipe/userroles/utils.d.ts deleted file mode 100644 index 4025b1b44..000000000 --- a/lib/build/recipe/userroles/utils.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -import { NormalisedAppinfo } from "../../types"; -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput } from "./types"; -export declare function validateAndNormaliseUserInput( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput; diff --git a/lib/build/recipe/userroles/utils.js b/lib/build/recipe/userroles/utils.js deleted file mode 100644 index 7023b84d5..000000000 --- a/lib/build/recipe/userroles/utils.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; -function validateAndNormaliseUserInput(_, __, config) { - let override = Object.assign( - { - functions: (originalImplementation) => originalImplementation, - apis: (originalImplementation) => originalImplementation, - }, - config === null || config === void 0 ? void 0 : config.override - ); - return { - skipAddingRolesToAccessToken: - (config === null || config === void 0 ? void 0 : config.skipAddingRolesToAccessToken) === true, - skipAddingPermissionsToAccessToken: - (config === null || config === void 0 ? void 0 : config.skipAddingPermissionsToAccessToken) === true, - override, - }; -} -exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipeModule.d.ts b/lib/build/recipeModule.d.ts deleted file mode 100644 index aef4fd4c8..000000000 --- a/lib/build/recipeModule.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -import STError from "./error"; -import { NormalisedAppinfo, APIHandled, HTTPMethod } from "./types"; -import NormalisedURLPath from "./normalisedURLPath"; -import { BaseRequest, BaseResponse } from "./framework"; -export default abstract class RecipeModule { - private recipeId; - private appInfo; - constructor(recipeId: string, appInfo: NormalisedAppinfo); - getRecipeId: () => string; - getAppInfo: () => NormalisedAppinfo; - returnAPIIdIfCanHandleRequest: (path: NormalisedURLPath, method: HTTPMethod) => string | undefined; - abstract getAPIsHandled(): APIHandled[]; - abstract handleAPIRequest( - id: string, - req: BaseRequest, - response: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ): Promise; - abstract handleError(error: STError, request: BaseRequest, response: BaseResponse): Promise; - abstract getAllCORSHeaders(): string[]; - abstract isErrorFromThisRecipe(err: any): err is STError; -} diff --git a/lib/build/recipeModule.js b/lib/build/recipeModule.js deleted file mode 100644 index efdd876f1..000000000 --- a/lib/build/recipeModule.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -class RecipeModule { - constructor(recipeId, appInfo) { - this.getRecipeId = () => { - return this.recipeId; - }; - this.getAppInfo = () => { - return this.appInfo; - }; - this.returnAPIIdIfCanHandleRequest = (path, method) => { - let apisHandled = this.getAPIsHandled(); - for (let i = 0; i < apisHandled.length; i++) { - let currAPI = apisHandled[i]; - if ( - !currAPI.disabled && - currAPI.method === method && - this.appInfo.apiBasePath.appendPath(currAPI.pathWithoutApiBasePath).equals(path) - ) { - return currAPI.id; - } - } - return undefined; - }; - this.recipeId = recipeId; - this.appInfo = appInfo; - } -} -exports.default = RecipeModule; diff --git a/lib/build/supertokens.d.ts b/lib/build/supertokens.d.ts deleted file mode 100644 index 682219bc4..000000000 --- a/lib/build/supertokens.d.ts +++ /dev/null @@ -1,93 +0,0 @@ -// @ts-nocheck -import { TypeInput, NormalisedAppinfo, HTTPMethod, SuperTokensInfo } from "./types"; -import RecipeModule from "./recipeModule"; -import NormalisedURLPath from "./normalisedURLPath"; -import { BaseRequest, BaseResponse } from "./framework"; -import { TypeFramework } from "./framework/types"; -export default class SuperTokens { - private static instance; - framework: TypeFramework; - appInfo: NormalisedAppinfo; - isInServerlessEnv: boolean; - recipeModules: RecipeModule[]; - supertokens: undefined | SuperTokensInfo; - telemetryEnabled: boolean; - constructor(config: TypeInput); - static init(config: TypeInput): void; - static reset(): void; - static getInstanceOrThrowError(): SuperTokens; - handleAPI: ( - matchedRecipe: RecipeModule, - id: string, - request: BaseRequest, - response: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ) => Promise; - getAllCORSHeaders: () => string[]; - getUserCount: (includeRecipeIds?: string[] | undefined) => Promise; - getUsers: (input: { - timeJoinedOrder: "ASC" | "DESC"; - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - }) => Promise<{ - users: { - recipeId: string; - user: any; - }[]; - nextPaginationToken?: string; - }>; - deleteUser: (input: { - userId: string; - }) => Promise<{ - status: "OK"; - }>; - createUserIdMapping: (input: { - superTokensUserId: string; - externalUserId: string; - externalUserIdInfo?: string; - force?: boolean; - }) => Promise< - | { - status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; - } - | { - status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"; - doesSuperTokensUserIdExist: boolean; - doesExternalUserIdExist: boolean; - } - >; - getUserIdMapping: (input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - }) => Promise< - | { - status: "OK"; - superTokensUserId: string; - externalUserId: string; - externalUserIdInfo: string | undefined; - } - | { - status: "UNKNOWN_MAPPING_ERROR"; - } - >; - deleteUserIdMapping: (input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - force?: boolean; - }) => Promise<{ - status: "OK"; - didMappingExist: boolean; - }>; - updateOrDeleteUserIdMappingInfo: (input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - externalUserIdInfo?: string; - }) => Promise<{ - status: "OK" | "UNKNOWN_MAPPING_ERROR"; - }>; - middleware: (request: BaseRequest, response: BaseResponse) => Promise; - errorHandler: (err: any, request: BaseRequest, response: BaseResponse) => Promise; -} diff --git a/lib/build/supertokens.js b/lib/build/supertokens.js deleted file mode 100644 index 3e06d0d9b..000000000 --- a/lib/build/supertokens.js +++ /dev/null @@ -1,375 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __awaiter = - (this && this.__awaiter) || - function (thisArg, _arguments, P, generator) { - function adopt(value) { - return value instanceof P - ? value - : new P(function (resolve) { - resolve(value); - }); - } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - } - function rejected(value) { - try { - step(generator["throw"](value)); - } catch (e) { - reject(e); - } - } - function step(result) { - result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); - } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("./utils"); -const querier_1 = require("./querier"); -const constants_1 = require("./constants"); -const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); -const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); -const error_1 = __importDefault(require("./error")); -const logger_1 = require("./logger"); -const postSuperTokensInitCallbacks_1 = require("./postSuperTokensInitCallbacks"); -class SuperTokens { - constructor(config) { - var _a, _b; - this.handleAPI = (matchedRecipe, id, request, response, path, method) => - __awaiter(this, void 0, void 0, function* () { - return yield matchedRecipe.handleAPIRequest(id, request, response, path, method); - }); - this.getAllCORSHeaders = () => { - let headerSet = new Set(); - headerSet.add(constants_1.HEADER_RID); - headerSet.add(constants_1.HEADER_FDI); - this.recipeModules.forEach((recipe) => { - let headers = recipe.getAllCORSHeaders(); - headers.forEach((h) => { - headerSet.add(h); - }); - }); - return Array.from(headerSet); - }; - this.getUserCount = (includeRecipeIds) => - __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error( - "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUserCount() instead (for example, EmailPassword.getUserCount())" - ); - } - let includeRecipeIdsStr = undefined; - if (includeRecipeIds !== undefined) { - includeRecipeIdsStr = includeRecipeIds.join(","); - } - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/users/count"), { - includeRecipeIds: includeRecipeIdsStr, - }); - return Number(response.count); - }); - this.getUsers = (input) => - __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error( - "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUsersOldestFirst() or .getUsersNewestFirst() instead (for example, EmailPassword.getUsersOldestFirst())" - ); - } - let includeRecipeIdsStr = undefined; - if (input.includeRecipeIds !== undefined) { - includeRecipeIdsStr = input.includeRecipeIds.join(","); - } - let response = yield querier.sendGetRequest( - new normalisedURLPath_1.default("/users"), - Object.assign(Object.assign({}, input.query), { - includeRecipeIds: includeRecipeIdsStr, - timeJoinedOrder: input.timeJoinedOrder, - limit: input.limit, - paginationToken: input.paginationToken, - }) - ); - return { - users: response.users, - nextPaginationToken: response.nextPaginationToken, - }; - }); - this.deleteUser = (input) => - __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.10", cdiVersion) === cdiVersion) { - // delete user is only available >= CDI 2.10 - yield querier.sendPostRequest(new normalisedURLPath_1.default("/user/remove"), { - userId: input.userId, - }); - return { - status: "OK", - }; - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.7.0"); - } - }); - this.createUserIdMapping = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - // create userId mapping is only available >= CDI 2.15 - return yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/userid/map"), { - superTokensUserId: input.superTokensUserId, - externalUserId: input.externalUserId, - externalUserIdInfo: input.externalUserIdInfo, - force: input.force, - }); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }); - }; - this.getUserIdMapping = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - // create userId mapping is only available >= CDI 2.15 - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/userid/map"), { - userId: input.userId, - userIdType: input.userIdType, - }); - return response; - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }); - }; - this.deleteUserIdMapping = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - return yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/userid/map/remove"), { - userId: input.userId, - userIdType: input.userIdType, - force: input.force, - }); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }); - }; - this.updateOrDeleteUserIdMappingInfo = function (input) { - return __awaiter(this, void 0, void 0, function* () { - let querier = querier_1.Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = yield querier.getAPIVersion(); - if (utils_1.maxVersion("2.15", cdiVersion) === cdiVersion) { - return yield querier.sendPutRequest( - new normalisedURLPath_1.default("/recipe/userid/external-user-id-info"), - { - userId: input.userId, - userIdType: input.userIdType, - externalUserIdInfo: input.externalUserIdInfo, - } - ); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }); - }; - this.middleware = (request, response) => - __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("middleware: Started"); - let path = this.appInfo.apiGatewayPath.appendPath( - new normalisedURLPath_1.default(request.getOriginalURL()) - ); - let method = utils_1.normaliseHttpMethod(request.getMethod()); - // if the prefix of the URL doesn't match the base path, we skip - if (!path.startsWith(this.appInfo.apiBasePath)) { - logger_1.logDebugMessage( - "middleware: Not handling because request path did not start with config path. Request path: " + - path.getAsStringDangerous() - ); - return false; - } - let requestRID = utils_1.getRidFromHeader(request); - logger_1.logDebugMessage("middleware: requestRID is: " + requestRID); - if (requestRID === "anti-csrf") { - // see https://github.com/supertokens/supertokens-node/issues/202 - requestRID = undefined; - } - if (requestRID !== undefined) { - let matchedRecipe = undefined; - // we loop through all recipe modules to find the one with the matching rId - for (let i = 0; i < this.recipeModules.length; i++) { - logger_1.logDebugMessage( - "middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId() - ); - if (this.recipeModules[i].getRecipeId() === requestRID) { - matchedRecipe = this.recipeModules[i]; - break; - } - } - if (matchedRecipe === undefined) { - logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); - // we could not find one, so we skip - return false; - } - logger_1.logDebugMessage("middleware: Matched with recipe ID: " + matchedRecipe.getRecipeId()); - let id = matchedRecipe.returnAPIIdIfCanHandleRequest(path, method); - if (id === undefined) { - logger_1.logDebugMessage( - "middleware: Not handling because recipe doesn't handle request path or method. Request path: " + - path.getAsStringDangerous() + - ", request method: " + - method - ); - // the matched recipe doesn't handle this path and http method - return false; - } - logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + id); - // give task to the matched recipe - let requestHandled = yield matchedRecipe.handleAPIRequest(id, request, response, path, method); - if (!requestHandled) { - logger_1.logDebugMessage( - "middleware: Not handled because API returned requestHandled as false" - ); - return false; - } - logger_1.logDebugMessage("middleware: Ended"); - return true; - } else { - // we loop through all recipe modules to find the one with the matching path and method - for (let i = 0; i < this.recipeModules.length; i++) { - logger_1.logDebugMessage( - "middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId() - ); - let id = this.recipeModules[i].returnAPIIdIfCanHandleRequest(path, method); - if (id !== undefined) { - logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + id); - let requestHandled = yield this.recipeModules[i].handleAPIRequest( - id, - request, - response, - path, - method - ); - if (!requestHandled) { - logger_1.logDebugMessage( - "middleware: Not handled because API returned requestHandled as false" - ); - return false; - } - logger_1.logDebugMessage("middleware: Ended"); - return true; - } - } - logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); - return false; - } - }); - this.errorHandler = (err, request, response) => - __awaiter(this, void 0, void 0, function* () { - logger_1.logDebugMessage("errorHandler: Started"); - if (error_1.default.isErrorFromSuperTokens(err)) { - logger_1.logDebugMessage("errorHandler: Error is from SuperTokens recipe. Message: " + err.message); - if (err.type === error_1.default.BAD_INPUT_ERROR) { - logger_1.logDebugMessage("errorHandler: Sending 400 status code response"); - return utils_1.sendNon200ResponseWithMessage(response, err.message, 400); - } - for (let i = 0; i < this.recipeModules.length; i++) { - logger_1.logDebugMessage( - "errorHandler: Checking recipe for match: " + this.recipeModules[i].getRecipeId() - ); - if (this.recipeModules[i].isErrorFromThisRecipe(err)) { - logger_1.logDebugMessage( - "errorHandler: Matched with recipeID: " + this.recipeModules[i].getRecipeId() - ); - return yield this.recipeModules[i].handleError(err, request, response); - } - } - } - throw err; - }); - logger_1.logDebugMessage("Started SuperTokens with debug logging (supertokens.init called)"); - logger_1.logDebugMessage("appInfo: " + JSON.stringify(config.appInfo)); - this.framework = config.framework !== undefined ? config.framework : "express"; - logger_1.logDebugMessage("framework: " + this.framework); - this.appInfo = utils_1.normaliseInputAppInfoOrThrowError(config.appInfo); - this.supertokens = config.supertokens; - querier_1.Querier.init( - (_a = config.supertokens) === null || _a === void 0 - ? void 0 - : _a.connectionURI - .split(";") - .filter((h) => h !== "") - .map((h) => { - return { - domain: new normalisedURLDomain_1.default(h.trim()), - basePath: new normalisedURLPath_1.default(h.trim()), - }; - }), - (_b = config.supertokens) === null || _b === void 0 ? void 0 : _b.apiKey - ); - if (config.recipeList === undefined || config.recipeList.length === 0) { - throw new Error("Please provide at least one recipe to the supertokens.init function call"); - } - // @ts-ignore - if (config.recipeList.includes(undefined)) { - // related to issue #270. If user makes mistake by adding empty items in the recipeList, this will catch the mistake and throw relevant error - throw new Error("Please remove empty items from recipeList"); - } - this.isInServerlessEnv = config.isInServerlessEnv === undefined ? false : config.isInServerlessEnv; - this.recipeModules = config.recipeList.map((func) => { - return func(this.appInfo, this.isInServerlessEnv); - }); - this.telemetryEnabled = config.telemetry === undefined ? process.env.TEST_MODE !== "testing" : config.telemetry; - } - static init(config) { - if (SuperTokens.instance === undefined) { - SuperTokens.instance = new SuperTokens(config); - postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.runPostInitCallbacks(); - } - } - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - querier_1.Querier.reset(); - SuperTokens.instance = undefined; - } - static getInstanceOrThrowError() { - if (SuperTokens.instance !== undefined) { - return SuperTokens.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } -} -exports.default = SuperTokens; diff --git a/lib/build/types.d.ts b/lib/build/types.d.ts deleted file mode 100644 index 424dbdb3c..000000000 --- a/lib/build/types.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -import RecipeModule from "./recipeModule"; -import NormalisedURLDomain from "./normalisedURLDomain"; -import NormalisedURLPath from "./normalisedURLPath"; -import { TypeFramework } from "./framework/types"; -export declare type AppInfo = { - appName: string; - websiteDomain: string; - websiteBasePath?: string; - apiDomain: string; - apiBasePath?: string; - apiGatewayPath?: string; -}; -export declare type NormalisedAppinfo = { - appName: string; - websiteDomain: NormalisedURLDomain; - apiDomain: NormalisedURLDomain; - topLevelAPIDomain: string; - topLevelWebsiteDomain: string; - apiBasePath: NormalisedURLPath; - apiGatewayPath: NormalisedURLPath; - websiteBasePath: NormalisedURLPath; -}; -export declare type SuperTokensInfo = { - connectionURI: string; - apiKey?: string; -}; -export declare type TypeInput = { - supertokens?: SuperTokensInfo; - framework?: TypeFramework; - appInfo: AppInfo; - recipeList: RecipeListFunction[]; - telemetry?: boolean; - isInServerlessEnv?: boolean; -}; -export declare type RecipeListFunction = (appInfo: NormalisedAppinfo, isInServerlessEnv: boolean) => RecipeModule; -export declare type APIHandled = { - pathWithoutApiBasePath: NormalisedURLPath; - method: HTTPMethod; - id: string; - disabled: boolean; -}; -export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; -export declare type JSONPrimitive = string | number | boolean | null; -export declare type JSONArray = Array; -export declare type JSONValue = JSONPrimitive | JSONObject | JSONArray | undefined; -export interface JSONObject { - [ind: string]: JSONValue; -} -export declare type GeneralErrorResponse = { - status: "GENERAL_ERROR"; - message: string; -}; diff --git a/lib/build/types.js b/lib/build/types.js deleted file mode 100644 index a098ca1d7..000000000 --- a/lib/build/types.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts deleted file mode 100644 index 7351e0e78..000000000 --- a/lib/build/utils.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject } from "./types"; -import type { BaseRequest, BaseResponse } from "./framework"; -export declare function getLargestVersionFromIntersection(v1: string[], v2: string[]): string | undefined; -export declare function maxVersion(version1: string, version2: string): string; -export declare function normaliseInputAppInfoOrThrowError(appInfo: AppInfo): NormalisedAppinfo; -export declare function normaliseHttpMethod(method: string): HTTPMethod; -export declare function sendNon200ResponseWithMessage(res: BaseResponse, message: string, statusCode: number): void; -export declare function sendNon200Response(res: BaseResponse, statusCode: number, body: JSONObject): void; -export declare function send200Response(res: BaseResponse, responseJson: any): void; -export declare function isAnIpAddress(ipaddress: string): boolean; -export declare function getRidFromHeader(req: BaseRequest): string | undefined; -export declare function frontendHasInterceptor(req: BaseRequest): boolean; -export declare function humaniseMilliseconds(ms: number): string; -export declare function makeDefaultUserContextFromAPI(request: BaseRequest): any; -export declare function getTopLevelDomainForSameSiteResolution(url: string): string; -export declare function getFromObjectCaseInsensitive(key: string, object: Record): T | undefined; diff --git a/lib/build/utils.js b/lib/build/utils.js deleted file mode 100644 index d3c17544f..000000000 --- a/lib/build/utils.js +++ /dev/null @@ -1,204 +0,0 @@ -"use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = void 0; -const psl = __importStar(require("psl")); -const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); -const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); -const logger_1 = require("./logger"); -const constants_1 = require("./constants"); -function getLargestVersionFromIntersection(v1, v2) { - let intersection = v1.filter((value) => v2.indexOf(value) !== -1); - if (intersection.length === 0) { - return undefined; - } - let maxVersionSoFar = intersection[0]; - for (let i = 1; i < intersection.length; i++) { - maxVersionSoFar = maxVersion(intersection[i], maxVersionSoFar); - } - return maxVersionSoFar; -} -exports.getLargestVersionFromIntersection = getLargestVersionFromIntersection; -function maxVersion(version1, version2) { - let splittedv1 = version1.split("."); - let splittedv2 = version2.split("."); - let minLength = Math.min(splittedv1.length, splittedv2.length); - for (let i = 0; i < minLength; i++) { - let v1 = Number(splittedv1[i]); - let v2 = Number(splittedv2[i]); - if (v1 > v2) { - return version1; - } else if (v2 > v1) { - return version2; - } - } - if (splittedv1.length >= splittedv2.length) { - return version1; - } - return version2; -} -exports.maxVersion = maxVersion; -function normaliseInputAppInfoOrThrowError(appInfo) { - if (appInfo === undefined) { - throw new Error("Please provide the appInfo object when calling supertokens.init"); - } - if (appInfo.apiDomain === undefined) { - throw new Error("Please provide your apiDomain inside the appInfo object when calling supertokens.init"); - } - if (appInfo.appName === undefined) { - throw new Error("Please provide your appName inside the appInfo object when calling supertokens.init"); - } - if (appInfo.websiteDomain === undefined) { - throw new Error("Please provide your websiteDomain inside the appInfo object when calling supertokens.init"); - } - let apiGatewayPath = - appInfo.apiGatewayPath !== undefined - ? new normalisedURLPath_1.default(appInfo.apiGatewayPath) - : new normalisedURLPath_1.default(""); - const websiteDomain = new normalisedURLDomain_1.default(appInfo.websiteDomain); - const apiDomain = new normalisedURLDomain_1.default(appInfo.apiDomain); - const topLevelAPIDomain = getTopLevelDomainForSameSiteResolution(apiDomain.getAsStringDangerous()); - const topLevelWebsiteDomain = getTopLevelDomainForSameSiteResolution(websiteDomain.getAsStringDangerous()); - return { - appName: appInfo.appName, - websiteDomain, - apiDomain, - apiBasePath: apiGatewayPath.appendPath( - appInfo.apiBasePath === undefined - ? new normalisedURLPath_1.default("/auth") - : new normalisedURLPath_1.default(appInfo.apiBasePath) - ), - websiteBasePath: - appInfo.websiteBasePath === undefined - ? new normalisedURLPath_1.default("/auth") - : new normalisedURLPath_1.default(appInfo.websiteBasePath), - apiGatewayPath, - topLevelAPIDomain, - topLevelWebsiteDomain, - }; -} -exports.normaliseInputAppInfoOrThrowError = normaliseInputAppInfoOrThrowError; -function normaliseHttpMethod(method) { - return method.toLowerCase(); -} -exports.normaliseHttpMethod = normaliseHttpMethod; -function sendNon200ResponseWithMessage(res, message, statusCode) { - sendNon200Response(res, statusCode, { message }); -} -exports.sendNon200ResponseWithMessage = sendNon200ResponseWithMessage; -function sendNon200Response(res, statusCode, body) { - if (statusCode < 300) { - throw new Error("Calling sendNon200Response with status code < 300"); - } - logger_1.logDebugMessage("Sending response to client with status code: " + statusCode); - res.setStatusCode(statusCode); - res.sendJSONResponse(body); -} -exports.sendNon200Response = sendNon200Response; -function send200Response(res, responseJson) { - logger_1.logDebugMessage("Sending response to client with status code: 200"); - res.setStatusCode(200); - res.sendJSONResponse(responseJson); -} -exports.send200Response = send200Response; -function isAnIpAddress(ipaddress) { - return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( - ipaddress - ); -} -exports.isAnIpAddress = isAnIpAddress; -function getRidFromHeader(req) { - return req.getHeaderValue(constants_1.HEADER_RID); -} -exports.getRidFromHeader = getRidFromHeader; -function frontendHasInterceptor(req) { - return getRidFromHeader(req) !== undefined; -} -exports.frontendHasInterceptor = frontendHasInterceptor; -function humaniseMilliseconds(ms) { - let t = Math.floor(ms / 1000); - let suffix = ""; - if (t < 60) { - if (t > 1) suffix = "s"; - return `${t} second${suffix}`; - } else if (t < 3600) { - const m = Math.floor(t / 60); - if (m > 1) suffix = "s"; - return `${m} minute${suffix}`; - } else { - const h = Math.floor(t / 360) / 10; - if (h > 1) suffix = "s"; - return `${h} hour${suffix}`; - } -} -exports.humaniseMilliseconds = humaniseMilliseconds; -function makeDefaultUserContextFromAPI(request) { - return { - _default: { - request, - }, - }; -} -exports.makeDefaultUserContextFromAPI = makeDefaultUserContextFromAPI; -function getTopLevelDomainForSameSiteResolution(url) { - let urlObj = new URL(url); - let hostname = urlObj.hostname; - if (hostname.startsWith("localhost") || hostname.startsWith("localhost.org") || isAnIpAddress(hostname)) { - // we treat these as the same TLDs since we can use sameSite lax for all of them. - return "localhost"; - } - let parsedURL = psl.parse(hostname); - if (parsedURL.domain === null) { - throw new Error("Please make sure that the apiDomain and websiteDomain have correct values"); - } - return parsedURL.domain; -} -exports.getTopLevelDomainForSameSiteResolution = getTopLevelDomainForSameSiteResolution; -function getFromObjectCaseInsensitive(key, object) { - const matchedKeys = Object.keys(object).filter((i) => i.toLocaleLowerCase() === key.toLocaleLowerCase()); - if (matchedKeys.length === 0) { - return undefined; - } - return object[matchedKeys[0]]; -} -exports.getFromObjectCaseInsensitive = getFromObjectCaseInsensitive; diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts deleted file mode 100644 index 69423839e..000000000 --- a/lib/build/version.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -export declare const version = "13.6.0"; -export declare const cdiSupported: string[]; -export declare const dashboardVersion = "0.6"; diff --git a/lib/build/version.js b/lib/build/version.js deleted file mode 100644 index 66276427e..000000000 --- a/lib/build/version.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -exports.version = "13.6.0"; -exports.cdiSupported = [ - "2.8", - "2.9", - "2.10", - "2.11", - "2.12", - "2.13", - "2.14", - "2.15", - "2.16", - "2.17", - "2.18", - "2.19", - "2.20", -]; -// Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} -exports.dashboardVersion = "0.6"; diff --git a/lib/ts/constants.ts b/lib/ts/constants.ts deleted file mode 100644 index 60565ce92..000000000 --- a/lib/ts/constants.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const HEADER_RID = "rid"; -export const HEADER_FDI = "fdi-version"; diff --git a/lib/ts/error.ts b/lib/ts/error.ts deleted file mode 100644 index dfcb1acc7..000000000 --- a/lib/ts/error.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export default class SuperTokensError extends Error { - private static errMagic = "ndskajfasndlfkj435234krjdsa"; - static BAD_INPUT_ERROR: "BAD_INPUT_ERROR" = "BAD_INPUT_ERROR"; - - public type: string; - public payload: any; - - // this variable is used to identify which - // recipe initiated this error. If no recipe - // initiated it, it will be undefined, else it - // will be the "actual" rid of that recipe. By actual, - // I mean that it will not be influenced by the - // parent's RID. - public fromRecipe: string | undefined; - // @ts-ignore - private errMagic: string; - - constructor( - options: - | { - message: string; - payload?: any; - type: string; - } - | { - message: string; - type: "BAD_INPUT_ERROR"; - payload: undefined; - } - ) { - super(options.message); - this.type = options.type; - this.payload = options.payload; - this.errMagic = SuperTokensError.errMagic; - } - - static isErrorFromSuperTokens(obj: any): obj is SuperTokensError { - return obj.errMagic === SuperTokensError.errMagic; - } -} diff --git a/lib/ts/framework/awsLambda/framework.ts b/lib/ts/framework/awsLambda/framework.ts deleted file mode 100644 index 811ae2248..000000000 --- a/lib/ts/framework/awsLambda/framework.ts +++ /dev/null @@ -1,367 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import type { - APIGatewayProxyEventV2, - APIGatewayProxyEvent, - APIGatewayProxyResult, - APIGatewayProxyStructuredResultV2, - Handler, - Context, - Callback, -} from "aws-lambda"; -import { HTTPMethod } from "../../types"; -import { getFromObjectCaseInsensitive, normaliseHttpMethod } from "../../utils"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { normalizeHeaderValue, getCookieValueFromHeaders, serializeCookieValue } from "../utils"; -import { COOKIE_HEADER } from "../constants"; -import { SessionContainerInterface } from "../../recipe/session/types"; -import SuperTokens from "../../supertokens"; -import { Framework } from "../types"; -import { parse } from "querystring"; -import { URL } from "url"; - -export class AWSRequest extends BaseRequest { - private event: APIGatewayProxyEventV2 | APIGatewayProxyEvent; - private parsedJSONBody: Object | undefined; - private parsedUrlEncodedFormData: Object | undefined; - - constructor(event: APIGatewayProxyEventV2 | APIGatewayProxyEvent) { - super(); - this.original = event; - this.event = event; - this.parsedJSONBody = undefined; - this.parsedUrlEncodedFormData = undefined; - } - - getFormData = async (): Promise => { - if (this.parsedUrlEncodedFormData === undefined) { - if (this.event.body === null || this.event.body === undefined) { - this.parsedUrlEncodedFormData = {}; - } else { - this.parsedUrlEncodedFormData = parse(this.event.body); - if (this.parsedUrlEncodedFormData === undefined) { - this.parsedUrlEncodedFormData = {}; - } - } - } - return this.parsedUrlEncodedFormData; - }; - - getKeyValueFromQuery = (key: string): string | undefined => { - if (this.event.queryStringParameters === undefined || this.event.queryStringParameters === null) { - return undefined; - } - let value = this.event.queryStringParameters[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - - getJSONBody = async (): Promise => { - if (this.parsedJSONBody === undefined) { - if (this.event.body === null || this.event.body === undefined) { - this.parsedJSONBody = {}; - } else { - this.parsedJSONBody = JSON.parse(this.event.body); - if (this.parsedJSONBody === undefined) { - this.parsedJSONBody = {}; - } - } - } - return this.parsedJSONBody; - }; - - getMethod = (): HTTPMethod => { - let rawMethod = (this.event as APIGatewayProxyEvent).httpMethod; - if (rawMethod !== undefined) { - return normaliseHttpMethod(rawMethod); - } - return normaliseHttpMethod((this.event as APIGatewayProxyEventV2).requestContext.http.method); - }; - - getCookieValue = (key: string): string | undefined => { - let cookies = (this.event as APIGatewayProxyEventV2).cookies; - if ( - (this.event.headers === undefined || this.event.headers === null) && - (cookies === undefined || cookies === null) - ) { - return undefined; - } - let value = getCookieValueFromHeaders(this.event.headers, key); - if (value === undefined && cookies !== undefined && cookies !== null) { - value = getCookieValueFromHeaders( - { - cookie: cookies.join(";"), - }, - key - ); - } - return value; - }; - - getHeaderValue = (key: string): string | undefined => { - if (this.event.headers === undefined || this.event.headers === null) { - return undefined; - } - return normalizeHeaderValue(getFromObjectCaseInsensitive(key, this.event.headers)); - }; - - getOriginalURL = (): string => { - let path = (this.event as APIGatewayProxyEvent).path; - let queryParams = (this.event as APIGatewayProxyEvent).queryStringParameters as { [key: string]: string }; - if (path === undefined) { - path = (this.event as APIGatewayProxyEventV2).requestContext.http.path; - let stage = (this.event as APIGatewayProxyEventV2).requestContext.stage; - if (stage !== undefined && path.startsWith(`/${stage}`)) { - path = path.slice(stage.length + 1); - } - if (queryParams !== undefined && queryParams !== null) { - let urlString = "https://exmaple.com" + path; - let url = new URL(urlString); - Object.keys(queryParams).forEach((el) => url.searchParams.append(el, queryParams[el])); - path = url.pathname + url.search; - } - } - return path; - }; -} - -interface SupertokensLambdaEvent extends APIGatewayProxyEvent { - supertokens: { - response: { - headers: { key: string; value: boolean | number | string; allowDuplicateKey: boolean }[]; - cookies: string[]; - }; - }; -} - -interface SupertokensLambdaEventV2 extends APIGatewayProxyEventV2 { - supertokens: { - response: { - headers: { key: string; value: boolean | number | string; allowDuplicateKey: boolean }[]; - cookies: string[]; - }; - }; -} - -export class AWSResponse extends BaseResponse { - private statusCode: number; - private event: SupertokensLambdaEvent | SupertokensLambdaEventV2; - private content: string; - public responseSet: boolean; - public statusSet: boolean; - - constructor(event: SupertokensLambdaEvent | SupertokensLambdaEventV2) { - super(); - this.original = event; - this.event = event; - this.statusCode = 200; - this.content = JSON.stringify({}); - this.responseSet = false; - this.statusSet = false; - this.event.supertokens = { - response: { - headers: [], - cookies: [], - }, - }; - } - - sendHTMLResponse = (html: string) => { - if (!this.responseSet) { - this.content = html; - this.setHeader("Content-Type", "text/html", false); - this.responseSet = true; - } - }; - - setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { - this.event.supertokens.response.headers.push({ - key, - value, - allowDuplicateKey, - }); - }; - - removeHeader = (key: string) => { - this.event.supertokens.response.headers = this.event.supertokens.response.headers.filter( - (header) => header.key.toLowerCase() !== key.toLowerCase() - ); - }; - - setCookie = ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => { - let serialisedCookie = serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite); - this.event.supertokens.response.cookies.push(serialisedCookie); - }; - - /** - * @param {number} statusCode - */ - setStatusCode = (statusCode: number) => { - if (!this.statusSet) { - this.statusCode = statusCode; - this.statusSet = true; - } - }; - - sendJSONResponse = (content: any) => { - if (!this.responseSet) { - this.content = JSON.stringify(content); - this.setHeader("Content-Type", "application/json", false); - this.responseSet = true; - } - }; - - sendResponse = (response?: APIGatewayProxyResult | APIGatewayProxyStructuredResultV2) => { - if (response === undefined) { - response = {}; - } - let headers: - | { - [header: string]: boolean | number | string; - } - | undefined = response.headers; - if (headers === undefined) { - headers = {}; - } - let body = response.body; - let statusCode = response.statusCode; - if (this.responseSet) { - statusCode = this.statusCode; - body = this.content; - } - let supertokensHeaders = this.event.supertokens.response.headers; - let supertokensCookies = this.event.supertokens.response.cookies; - for (let i = 0; i < supertokensHeaders.length; i++) { - let currentValue = undefined; - let currentHeadersSet = Object.keys(headers === undefined ? [] : headers); - for (let j = 0; j < currentHeadersSet.length; j++) { - if (currentHeadersSet[j].toLowerCase() === supertokensHeaders[i].key.toLowerCase()) { - supertokensHeaders[i].key = currentHeadersSet[j]; - currentValue = headers[currentHeadersSet[j]]; - break; - } - } - if (supertokensHeaders[i].allowDuplicateKey && currentValue !== undefined) { - let newValue = `${currentValue}, ${supertokensHeaders[i].value}`; - headers[supertokensHeaders[i].key] = newValue; - } else { - headers[supertokensHeaders[i].key] = supertokensHeaders[i].value; - } - } - if ((this.event as APIGatewayProxyEventV2).version !== undefined) { - let cookies = (response as APIGatewayProxyStructuredResultV2).cookies; - if (cookies === undefined) { - cookies = []; - } - cookies.push(...supertokensCookies); - - let result: APIGatewayProxyStructuredResultV2 = { - ...(response as APIGatewayProxyStructuredResultV2), - cookies, - body, - statusCode, - headers, - }; - return result; - } else { - let multiValueHeaders = (response as APIGatewayProxyResult).multiValueHeaders; - if (multiValueHeaders === undefined) { - multiValueHeaders = {}; - } - let headsersInMultiValueHeaders = Object.keys(multiValueHeaders); - let cookieHeader = headsersInMultiValueHeaders.find((h) => h.toLowerCase() === COOKIE_HEADER.toLowerCase()); - if (cookieHeader === undefined) { - multiValueHeaders[COOKIE_HEADER] = supertokensCookies; - } else { - multiValueHeaders[cookieHeader].push(...supertokensCookies); - } - let result: APIGatewayProxyResult = { - ...(response as APIGatewayProxyResult), - multiValueHeaders, - body: body as string, - statusCode: statusCode as number, - headers, - }; - return result; - } - }; -} - -export interface SessionEventV2 extends SupertokensLambdaEventV2 { - session?: SessionContainerInterface; -} - -export interface SessionEvent extends SupertokensLambdaEvent { - session?: SessionContainerInterface; -} - -export const middleware = (handler?: Handler): Handler => { - return async (event: SessionEvent | SessionEventV2, context: Context, callback: Callback) => { - let supertokens = SuperTokens.getInstanceOrThrowError(); - let request = new AWSRequest(event); - let response = new AWSResponse(event); - try { - let result = await supertokens.middleware(request, response); - if (result) { - return response.sendResponse(); - } - if (handler !== undefined) { - let handlerResult = await handler(event, context, callback); - return response.sendResponse(handlerResult); - } - /** - * it reaches this point only if the API route was not exposed by - * the SDK and user didn't provide a handler - */ - response.setStatusCode(404); - response.sendJSONResponse({ - error: `The middleware couldn't serve the API path ${request.getOriginalURL()}, method: ${request.getMethod()}. If this is an unexpected behaviour, please create an issue here: https://github.com/supertokens/supertokens-node/issues`, - }); - return response.sendResponse(); - } catch (err) { - await supertokens.errorHandler(err, request, response); - if (response.responseSet) { - return response.sendResponse(); - } - throw err; - } - }; -}; - -export interface AWSFramework extends Framework { - middleware: (handler?: Handler) => Handler; -} - -export const AWSWrapper: AWSFramework = { - middleware, - wrapRequest: (unwrapped) => { - return new AWSRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new AWSResponse(unwrapped); - }, -}; diff --git a/lib/ts/framework/awsLambda/index.ts b/lib/ts/framework/awsLambda/index.ts deleted file mode 100644 index 9882af9ee..000000000 --- a/lib/ts/framework/awsLambda/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { AWSWrapper } from "./framework"; -export type { SessionEvent, SessionEventV2 } from "./framework"; - -export const middleware = AWSWrapper.middleware; -export const wrapRequest = AWSWrapper.wrapRequest; -export const wrapResponse = AWSWrapper.wrapResponse; diff --git a/lib/ts/framework/constants.ts b/lib/ts/framework/constants.ts deleted file mode 100644 index 603783285..000000000 --- a/lib/ts/framework/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export const COOKIE_HEADER = "Set-Cookie"; diff --git a/lib/ts/framework/express/framework.ts b/lib/ts/framework/express/framework.ts deleted file mode 100644 index a838a0612..000000000 --- a/lib/ts/framework/express/framework.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import type { Request, Response, NextFunction } from "express"; -import type { HTTPMethod } from "../../types"; -import { normaliseHttpMethod } from "../../utils"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { - setCookieForServerResponse, - setHeaderForExpressLikeResponse, - getCookieValueFromIncomingMessage, - getHeaderValueFromIncomingMessage, - assertThatBodyParserHasBeenUsedForExpressLikeRequest, - assertFormDataBodyParserHasBeenUsedForExpressLikeRequest, -} from "../utils"; -import type { Framework } from "../types"; -import SuperTokens from "../../supertokens"; -import type { SessionContainerInterface } from "../../recipe/session/types"; - -export class ExpressRequest extends BaseRequest { - private request: Request; - private parserChecked: boolean; - private formDataParserChecked: boolean; - - constructor(request: Request) { - super(); - this.original = request; - this.request = request; - this.parserChecked = false; - this.formDataParserChecked = false; - } - - getFormData = async (): Promise => { - if (!this.formDataParserChecked) { - await assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); - this.formDataParserChecked = true; - } - return this.request.body; - }; - - getKeyValueFromQuery = (key: string): string | undefined => { - if (this.request.query === undefined) { - return undefined; - } - let value = this.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - - getJSONBody = async (): Promise => { - if (!this.parserChecked) { - await assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); - this.parserChecked = true; - } - return this.request.body; - }; - - getMethod = (): HTTPMethod => { - return normaliseHttpMethod(this.request.method); - }; - - getCookieValue = (key: string): string | undefined => { - return getCookieValueFromIncomingMessage(this.request, key); - }; - - getHeaderValue = (key: string): string | undefined => { - return getHeaderValueFromIncomingMessage(this.request, key); - }; - - getOriginalURL = (): string => { - return this.request.originalUrl || this.request.url; - }; -} - -export class ExpressResponse extends BaseResponse { - private response: Response; - private statusCode: number; - - constructor(response: Response) { - super(); - this.original = response; - this.response = response; - this.statusCode = 200; - } - - sendHTMLResponse = (html: string) => { - if (!this.response.writableEnded) { - /** - * response.set method is not available if response - * is a nextjs response object. setHeader method - * is present on OutgoingMessage which is one of the - * bases used to construct response object for express - * like response as well as nextjs like response - */ - this.response.setHeader("Content-Type", "text/html"); - this.response.status(this.statusCode).send(Buffer.from(html)); - } - }; - - setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { - setHeaderForExpressLikeResponse(this.response, key, value, allowDuplicateKey); - }; - - removeHeader = (key: string) => { - this.response.removeHeader(key); - }; - - setCookie = ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => { - setCookieForServerResponse(this.response, key, value, domain, secure, httpOnly, expires, path, sameSite); - }; - - /** - * @param {number} statusCode - */ - setStatusCode = (statusCode: number) => { - if (!this.response.writableEnded) { - this.statusCode = statusCode; - } - }; - - sendJSONResponse = (content: any) => { - if (!this.response.writableEnded) { - this.response.status(this.statusCode).json(content); - } - }; -} - -export interface SessionRequest extends Request { - session?: SessionContainerInterface; -} - -export const middleware = () => { - return async (req: Request, res: Response, next: NextFunction) => { - let supertokens; - const request = new ExpressRequest(req); - const response = new ExpressResponse(res); - try { - supertokens = SuperTokens.getInstanceOrThrowError(); - const result = await supertokens.middleware(request, response); - if (!result) { - return next(); - } - } catch (err) { - if (supertokens) { - try { - await supertokens.errorHandler(err, request, response); - } catch { - next(err); - } - } else { - next(err); - } - } - }; -}; -export const errorHandler = () => { - return async (err: any, req: Request, res: Response, next: NextFunction) => { - let supertokens = SuperTokens.getInstanceOrThrowError(); - let request = new ExpressRequest(req); - let response = new ExpressResponse(res); - try { - await supertokens.errorHandler(err, request, response); - } catch (err) { - return next(err); - } - }; -}; - -export interface ExpressFramework extends Framework { - middleware: () => (req: Request, res: Response, next: NextFunction) => Promise; - errorHandler: () => (err: any, req: Request, res: Response, next: NextFunction) => Promise; -} - -export const ExpressWrapper: ExpressFramework = { - middleware, - errorHandler, - wrapRequest: (unwrapped) => { - return new ExpressRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new ExpressResponse(unwrapped); - }, -}; diff --git a/lib/ts/framework/express/index.ts b/lib/ts/framework/express/index.ts deleted file mode 100644 index 6fa97da4f..000000000 --- a/lib/ts/framework/express/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { ExpressWrapper } from "./framework"; -export type { SessionRequest } from "./framework"; - -export const middleware = ExpressWrapper.middleware; -export const errorHandler = ExpressWrapper.errorHandler; -export const wrapRequest = ExpressWrapper.wrapRequest; -export const wrapResponse = ExpressWrapper.wrapResponse; diff --git a/lib/ts/framework/fastify/framework.ts b/lib/ts/framework/fastify/framework.ts deleted file mode 100644 index 65ccf03ae..000000000 --- a/lib/ts/framework/fastify/framework.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import type { - FastifyInstance, - FastifyRequest as OriginalFastifyRequest, - FastifyReply, - FastifyPluginCallback, -} from "fastify"; -import type { HTTPMethod } from "../../types"; -import { getFromObjectCaseInsensitive, normaliseHttpMethod } from "../../utils"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { serializeCookieValue, normalizeHeaderValue, getCookieValueFromHeaders } from "../utils"; -import type { Framework } from "../types"; -import SuperTokens from "../../supertokens"; -import type { SessionContainerInterface } from "../../recipe/session/types"; -import { COOKIE_HEADER } from "../constants"; - -export class FastifyRequest extends BaseRequest { - private request: OriginalFastifyRequest; - - constructor(request: OriginalFastifyRequest) { - super(); - this.original = request; - this.request = request; - } - - getFormData = async (): Promise => { - return this.request.body; // NOTE: ask user to add require('fastify-formbody') - }; - - getKeyValueFromQuery = (key: string): string | undefined => { - if (this.request.query === undefined) { - return undefined; - } - let value = (this.request.query as any)[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - - getJSONBody = async (): Promise => { - return this.request.body; - }; - - getMethod = (): HTTPMethod => { - return normaliseHttpMethod(this.request.method); - }; - - getCookieValue = (key: string): string | undefined => { - return getCookieValueFromHeaders(this.request.headers, key); - }; - - getHeaderValue = (key: string): string | undefined => { - return normalizeHeaderValue(getFromObjectCaseInsensitive(key, this.request.headers)); - }; - - getOriginalURL = (): string => { - return this.request.url; - }; -} - -export class FastifyResponse extends BaseResponse { - private response: FastifyReply; - private statusCode: number; - - constructor(response: FastifyReply) { - super(); - this.original = response; - this.response = response; - this.statusCode = 200; - } - - sendHTMLResponse = (html: string) => { - if (!this.response.sent) { - this.response.type("text/html"); - this.response.send(html); - } - }; - setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { - try { - let existingHeaders = this.response.getHeaders(); - let existingValue = existingHeaders[key.toLowerCase()]; - - // we have the this.response.header for compatibility with nextJS - if (existingValue === undefined) { - this.response.header(key, value); - } else if (allowDuplicateKey) { - this.response.header(key, existingValue + ", " + value); - } else { - // we overwrite the current one with the new one - this.response.header(key, value); - } - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } - }; - - removeHeader = (key: string) => { - this.response.removeHeader(key); - }; - - setCookie = ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => { - let serialisedCookie = serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite); - /** - * lets say if current value is undefined, prev -> undefined - * - * now if add AT, - * cookieValueToSetInHeader -> AT - * response header object will be: - * - * 'set-cookie': AT - * - * now if add RT, - * - * prev -> AT - * cookieValueToSetInHeader -> AT + RT - * and response header object will be: - * - * 'set-cookie': AT + AT + RT - * - * now if add IRT, - * - * prev -> AT + AT + RT - * cookieValueToSetInHeader -> IRT + AT + AT + RT - * and response header object will be: - * - * 'set-cookie': AT + AT + RT + IRT + AT + AT + RT - * - * To avoid this, we no longer get and use the previous value - * - * Old code: - * - * let prev: string | string[] | undefined = this.response.getHeader(COOKIE_HEADER) as - * | string - * | string[] - * | undefined; - * let cookieValueToSetInHeader = getCookieValueToSetInHeader(prev, serialisedCookie, key); - * this.response.header(COOKIE_HEADER, cookieValueToSetInHeader); - */ - this.response.header(COOKIE_HEADER, serialisedCookie); - }; - - /** - * @param {number} statusCode - */ - setStatusCode = (statusCode: number) => { - if (!this.response.sent) { - this.statusCode = statusCode; - } - }; - - /** - * @param {any} content - */ - sendJSONResponse = (content: any) => { - if (!this.response.sent) { - this.response.statusCode = this.statusCode; - this.response.send(content); - } - }; -} - -function plugin(fastify: FastifyInstance, _: any, done: Function) { - fastify.addHook("preHandler", async (req: OriginalFastifyRequest, reply: FastifyReply) => { - let supertokens = SuperTokens.getInstanceOrThrowError(); - let request = new FastifyRequest(req); - let response = new FastifyResponse(reply); - try { - await supertokens.middleware(request, response); - } catch (err) { - await supertokens.errorHandler(err, request, response); - } - }); - done(); -} -(plugin as any)[Symbol.for("skip-override")] = true; - -export interface SessionRequest extends OriginalFastifyRequest { - session?: SessionContainerInterface; -} - -export interface FasitfyFramework extends Framework { - plugin: FastifyPluginCallback; - errorHandler: () => (err: any, req: OriginalFastifyRequest, res: FastifyReply) => Promise; -} - -export const errorHandler = () => { - return async (err: any, req: OriginalFastifyRequest, res: FastifyReply) => { - let supertokens = SuperTokens.getInstanceOrThrowError(); - let request = new FastifyRequest(req); - let response = new FastifyResponse(res); - await supertokens.errorHandler(err, request, response); - }; -}; - -export const FastifyWrapper: FasitfyFramework = { - plugin, - errorHandler, - wrapRequest: (unwrapped) => { - return new FastifyRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new FastifyResponse(unwrapped); - }, -}; diff --git a/lib/ts/framework/fastify/index.ts b/lib/ts/framework/fastify/index.ts deleted file mode 100644 index 2d06b9a10..000000000 --- a/lib/ts/framework/fastify/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { FastifyWrapper } from "./framework"; -export type { SessionRequest } from "./framework"; - -export const plugin = FastifyWrapper.plugin; -export const errorHandler = FastifyWrapper.errorHandler; -export const wrapRequest = FastifyWrapper.wrapRequest; -export const wrapResponse = FastifyWrapper.wrapResponse; diff --git a/lib/ts/framework/hapi/framework.ts b/lib/ts/framework/hapi/framework.ts deleted file mode 100644 index 40cc92ee9..000000000 --- a/lib/ts/framework/hapi/framework.ts +++ /dev/null @@ -1,281 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import type { Request, ResponseToolkit, Plugin, ResponseObject, ServerRoute } from "@hapi/hapi"; -import type { Boom } from "@hapi/boom"; -import type { HTTPMethod } from "../../types"; -import { normaliseHttpMethod } from "../../utils"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { normalizeHeaderValue, getCookieValueFromHeaders } from "../utils"; -import type { Framework } from "../types"; -import SuperTokens from "../../supertokens"; -import type { SessionContainerInterface } from "../../recipe/session/types"; - -export class HapiRequest extends BaseRequest { - private request: Request; - - constructor(request: Request) { - super(); - this.original = request; - this.request = request; - } - - getFormData = async (): Promise => { - return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; - }; - - getKeyValueFromQuery = (key: string): string | undefined => { - if (this.request.query === undefined) { - return undefined; - } - let value = this.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - - getJSONBody = async (): Promise => { - return this.request.payload === undefined || this.request.payload === null ? {} : this.request.payload; - }; - - getMethod = (): HTTPMethod => { - return normaliseHttpMethod(this.request.method); - }; - - getCookieValue = (key: string): string | undefined => { - return getCookieValueFromHeaders(this.request.headers, key); - }; - - getHeaderValue = (key: string): string | undefined => { - return normalizeHeaderValue(this.request.headers[key]); - }; - - getOriginalURL = (): string => { - return this.request.url.toString(); - }; -} - -export interface ExtendedResponseToolkit extends ResponseToolkit { - lazyHeaderBindings: ( - h: ResponseToolkit, - key: string, - value: string | undefined, - allowDuplicateKey: boolean - ) => void; -} - -export class HapiResponse extends BaseResponse { - private response: ExtendedResponseToolkit; - private statusCode: number; - private content: any; - public responseSet: boolean; - public statusSet = false; - - constructor(response: ExtendedResponseToolkit) { - super(); - this.original = response; - this.response = response; - this.statusCode = 200; - this.content = null; - this.responseSet = false; - } - - sendHTMLResponse = (html: string) => { - if (!this.responseSet) { - this.content = html; - this.setHeader("Content-Type", "text/html", false); - this.responseSet = true; - } - }; - - setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { - try { - this.response.lazyHeaderBindings(this.response, key, value, allowDuplicateKey); - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } - }; - - removeHeader = (key: string) => { - this.response.lazyHeaderBindings(this.response, key, undefined, false); - }; - - setCookie = ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => { - let now = Date.now(); - - if (expires > now) { - this.response.state(key, value, { - isHttpOnly: httpOnly, - isSecure: secure, - path: path, - domain, - ttl: expires - now, - isSameSite: sameSite === "lax" ? "Lax" : sameSite === "none" ? "None" : "Strict", - }); - } else { - this.response.unstate(key); - } - }; - - /** - * @param {number} statusCode - */ - setStatusCode = (statusCode: number) => { - if (!this.statusSet) { - this.statusCode = statusCode; - this.statusSet = true; - } - }; - - /** - * @param {any} content - */ - sendJSONResponse = (content: any) => { - if (!this.responseSet) { - this.content = content; - this.responseSet = true; - } - }; - - sendResponse = (overwriteHeaders = false): ResponseObject => { - if (!overwriteHeaders) { - return this.response.response(this.content).code(this.statusCode).takeover(); - } - return this.response.response(this.content).code(this.statusCode); - }; -} - -const plugin: Plugin<{}> = { - name: "supertokens-hapi-middleware", - version: "1.0.0", - register: async function (server, _) { - let supertokens = SuperTokens.getInstanceOrThrowError(); - server.ext("onPreHandler", async (req, h) => { - let request = new HapiRequest(req); - let response = new HapiResponse(h as ExtendedResponseToolkit); - let result = await supertokens.middleware(request, response); - if (!result) { - return h.continue; - } - return response.sendResponse(); - }); - server.ext("onPreResponse", async (request, h) => { - (((request.app as any).lazyHeaders || []) as { - key: string; - value: string; - allowDuplicateKey: boolean; - }[]).forEach(({ key, value, allowDuplicateKey }) => { - if ((request.response as Boom).isBoom) { - (request.response as Boom).output.headers[key] = value; - } else { - (request.response as ResponseObject).header(key, value, { append: allowDuplicateKey }); - } - }); - if ((request.response as Boom).isBoom) { - let err = (request.response as Boom).data || request.response; - let req = new HapiRequest(request); - let res = new HapiResponse(h as ExtendedResponseToolkit); - if (err !== undefined && err !== null) { - try { - await supertokens.errorHandler(err, req, res); - if (res.responseSet) { - let resObj = res.sendResponse(true); - (((request.app as any).lazyHeaders || []) as { - key: string; - value: string; - allowDuplicateKey: boolean; - }[]).forEach(({ key, value, allowDuplicateKey }) => { - resObj.header(key, value, { append: allowDuplicateKey }); - }); - return resObj.takeover(); - } - return h.continue; - } catch (e) { - return h.continue; - } - } - } - return h.continue; - }); - server.decorate("toolkit", "lazyHeaderBindings", function ( - h: ResponseToolkit, - key: string, - value: string | undefined, - allowDuplicateKey: boolean - ) { - const anyApp = h.request.app as any; - anyApp.lazyHeaders = anyApp.lazyHeaders || []; - if (value === undefined) { - anyApp.lazyHeaders = anyApp.lazyHeaders.filter( - (header: { key: string }) => header.key.toLowerCase() !== key.toLowerCase() - ); - } else { - anyApp.lazyHeaders.push({ key, value, allowDuplicateKey }); - } - }); - let supportedRoutes: ServerRoute[] = []; - let routeMethodSet = new Set(); - for (let i = 0; i < supertokens.recipeModules.length; i++) { - let apisHandled = supertokens.recipeModules[i].getAPIsHandled(); - for (let j = 0; j < apisHandled.length; j++) { - let api = apisHandled[j]; - if (!api.disabled) { - let path = `${supertokens.appInfo.apiBasePath.getAsStringDangerous()}${api.pathWithoutApiBasePath.getAsStringDangerous()}`; - let methodAndPath = `${api.method}-${path}`; - if (!routeMethodSet.has(methodAndPath)) { - supportedRoutes.push({ - path, - method: api.method, - handler: (_, h) => { - return h.continue; - }, - }); - routeMethodSet.add(methodAndPath); - } - } - } - } - server.route(supportedRoutes); - }, -}; - -export interface SessionRequest extends Request { - session?: SessionContainerInterface; -} - -export interface HapiFramework extends Framework { - plugin: Plugin<{}>; -} - -export const HapiWrapper: HapiFramework = { - plugin, - wrapRequest: (unwrapped) => { - return new HapiRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new HapiResponse(unwrapped); - }, -}; diff --git a/lib/ts/framework/hapi/index.ts b/lib/ts/framework/hapi/index.ts deleted file mode 100644 index 045986ef2..000000000 --- a/lib/ts/framework/hapi/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { HapiWrapper } from "./framework"; -export type { SessionRequest } from "./framework"; - -export const plugin = HapiWrapper.plugin; -export const wrapRequest = HapiWrapper.wrapRequest; -export const wrapResponse = HapiWrapper.wrapResponse; diff --git a/lib/ts/framework/index.ts b/lib/ts/framework/index.ts deleted file mode 100644 index 3d38c264a..000000000 --- a/lib/ts/framework/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export { BaseRequest } from "./request"; -export { BaseResponse } from "./response"; - -import * as expressFramework from "./express"; -import * as fastifyFramework from "./fastify"; -import * as hapiFramework from "./hapi"; -import * as loopbackFramework from "./loopback"; -import * as koaFramework from "./koa"; -import * as awsLambdaFramework from "./awsLambda"; - -export default { - express: expressFramework, - fastify: fastifyFramework, - hapi: hapiFramework, - loopback: loopbackFramework, - koa: koaFramework, - awsLambda: awsLambdaFramework, -}; - -export let express = expressFramework; -export let fastify = fastifyFramework; -export let hapi = hapiFramework; -export let loopback = loopbackFramework; -export let koa = koaFramework; -export let awsLambda = awsLambdaFramework; diff --git a/lib/ts/framework/koa/framework.ts b/lib/ts/framework/koa/framework.ts deleted file mode 100644 index 721a57ae0..000000000 --- a/lib/ts/framework/koa/framework.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import type { Context, Next } from "koa"; -import type { HTTPMethod } from "../../types"; -import { normaliseHttpMethod } from "../../utils"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { getHeaderValueFromIncomingMessage } from "../utils"; -import { json, form } from "co-body"; -import { SessionContainerInterface } from "../../recipe/session/types"; -import SuperTokens from "../../supertokens"; -import { Framework } from "../types"; - -export class KoaRequest extends BaseRequest { - private ctx: Context; - private parsedJSONBody: Object | undefined; - private parsedUrlEncodedFormData: Object | undefined; - - constructor(ctx: Context) { - super(); - this.original = ctx; - this.ctx = ctx; - this.parsedJSONBody = undefined; - this.parsedUrlEncodedFormData = undefined; - } - - getFormData = async (): Promise => { - if (this.parsedUrlEncodedFormData === undefined) { - this.parsedUrlEncodedFormData = await parseURLEncodedFormData(this.ctx); - } - return this.parsedUrlEncodedFormData; - }; - - getKeyValueFromQuery = (key: string): string | undefined => { - if (this.ctx.query === undefined) { - return undefined; - } - let value = this.ctx.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - - getJSONBody = async (): Promise => { - if (this.parsedJSONBody === undefined) { - this.parsedJSONBody = await parseJSONBodyFromRequest(this.ctx); - } - return this.parsedJSONBody === undefined ? {} : this.parsedJSONBody; - }; - - getMethod = (): HTTPMethod => { - return normaliseHttpMethod(this.ctx.request.method); - }; - - getCookieValue = (key: string): string | undefined => { - return this.ctx.cookies.get(key); - }; - - getHeaderValue = (key: string): string | undefined => { - return getHeaderValueFromIncomingMessage(this.ctx.req, key); - }; - - getOriginalURL = (): string => { - return this.ctx.originalUrl; - }; -} - -async function parseJSONBodyFromRequest(ctx: Context) { - if (ctx.body !== undefined) { - return ctx.body; - } - return await json(ctx); -} - -async function parseURLEncodedFormData(ctx: Context) { - if (ctx.body !== undefined) { - return ctx.body; - } - return await form(ctx); -} - -export class KoaResponse extends BaseResponse { - private ctx: Context; - public responseSet: boolean = false; - public statusSet = false; - - constructor(ctx: Context) { - super(); - this.original = ctx; - this.ctx = ctx; - } - - sendHTMLResponse = (html: string) => { - if (!this.responseSet) { - this.ctx.set("content-type", "text/html"); - this.ctx.body = html; - this.responseSet = true; - } - }; - - setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { - try { - let existingHeaders = this.ctx.response.headers; - let existingValue = existingHeaders[key.toLowerCase()]; - - if (existingValue === undefined) { - this.ctx.set(key, value); - } else if (allowDuplicateKey) { - this.ctx.set(key, existingValue + ", " + value); - } else { - // we overwrite the current one with the new one - this.ctx.set(key, value); - } - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } - }; - - removeHeader = (key: string) => { - this.ctx.remove(key); - }; - - setCookie = ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => { - this.ctx.cookies.set(key, value, { - secure, - sameSite, - httpOnly, - expires: new Date(expires), - domain, - path, - }); - }; - - /** - * @param {number} statusCode - */ - setStatusCode = (statusCode: number) => { - if (!this.statusSet) { - this.ctx.status = statusCode; - this.statusSet = true; - } - }; - - sendJSONResponse = (content: any) => { - if (!this.responseSet) { - this.ctx.body = content; - this.responseSet = true; - } - }; -} - -export interface SessionContext extends Context { - session?: SessionContainerInterface; -} - -export const middleware = () => { - return async (ctx: Context, next: Next) => { - let supertokens = SuperTokens.getInstanceOrThrowError(); - let request = new KoaRequest(ctx); - let response = new KoaResponse(ctx); - try { - let result = await supertokens.middleware(request, response); - if (!result) { - return await next(); - } - } catch (err) { - return await supertokens.errorHandler(err, request, response); - } - }; -}; - -export interface KoaFramework extends Framework { - middleware: () => (ctx: Context, next: Next) => Promise; -} - -export const KoaWrapper: KoaFramework = { - middleware, - wrapRequest: (unwrapped) => { - return new KoaRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new KoaResponse(unwrapped); - }, -}; diff --git a/lib/ts/framework/koa/index.ts b/lib/ts/framework/koa/index.ts deleted file mode 100644 index 5783ec4dd..000000000 --- a/lib/ts/framework/koa/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { KoaWrapper } from "./framework"; -export type { SessionContext } from "./framework"; - -export const middleware = KoaWrapper.middleware; -export const wrapRequest = KoaWrapper.wrapRequest; -export const wrapResponse = KoaWrapper.wrapResponse; diff --git a/lib/ts/framework/loopback/framework.ts b/lib/ts/framework/loopback/framework.ts deleted file mode 100644 index 86a740f79..000000000 --- a/lib/ts/framework/loopback/framework.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import type { MiddlewareContext, Request, Response, Middleware } from "@loopback/rest"; -import type { Next } from "@loopback/core"; -import { SessionContainerInterface } from "../../recipe/session/types"; -import { HTTPMethod } from "../../types"; -import { normaliseHttpMethod } from "../../utils"; -import { BaseRequest } from "../request"; -import { BaseResponse } from "../response"; -import { - getCookieValueFromIncomingMessage, - getHeaderValueFromIncomingMessage, - assertThatBodyParserHasBeenUsedForExpressLikeRequest, - setHeaderForExpressLikeResponse, - setCookieForServerResponse, - assertFormDataBodyParserHasBeenUsedForExpressLikeRequest, -} from "../utils"; -import SuperTokens from "../../supertokens"; -import type { Framework } from "../types"; - -export class LoopbackRequest extends BaseRequest { - private request: Request; - private parserChecked: boolean; - private formDataParserChecked: boolean; - - constructor(ctx: MiddlewareContext) { - super(); - this.original = ctx.request; - this.request = ctx.request; - this.parserChecked = false; - this.formDataParserChecked = false; - } - - getFormData = async (): Promise => { - if (!this.formDataParserChecked) { - await assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request); - this.formDataParserChecked = true; - } - return this.request.body; - }; - - getKeyValueFromQuery = (key: string): string | undefined => { - if (this.request.query === undefined) { - return undefined; - } - let value = this.request.query[key]; - if (value === undefined || typeof value !== "string") { - return undefined; - } - return value; - }; - - getJSONBody = async (): Promise => { - if (!this.parserChecked) { - await assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request); - this.parserChecked = true; - } - return this.request.body; - }; - - getMethod = (): HTTPMethod => { - return normaliseHttpMethod(this.request.method); - }; - - getCookieValue = (key: string): string | undefined => { - return getCookieValueFromIncomingMessage(this.request, key); - }; - - getHeaderValue = (key: string): string | undefined => { - return getHeaderValueFromIncomingMessage(this.request, key); - }; - - getOriginalURL = (): string => { - return this.request.originalUrl; - }; -} - -export class LoopbackResponse extends BaseResponse { - response: Response; - private statusCode: number; - - constructor(ctx: MiddlewareContext) { - super(); - this.original = ctx.response; - this.response = ctx.response; - this.statusCode = 200; - } - - sendHTMLResponse = (html: string) => { - if (!this.response.writableEnded) { - this.response.set("Content-Type", "text/html"); - this.response.status(this.statusCode).send(Buffer.from(html)); - } - }; - - setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { - setHeaderForExpressLikeResponse(this.response, key, value, allowDuplicateKey); - }; - - removeHeader = (key: string) => { - this.response.removeHeader(key); - }; - - setCookie = ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => { - setCookieForServerResponse(this.response, key, value, domain, secure, httpOnly, expires, path, sameSite); - }; - - setStatusCode = (statusCode: number) => { - if (!this.response.writableEnded) { - this.statusCode = statusCode; - } - }; - sendJSONResponse = (content: any) => { - if (!this.response.writableEnded) { - this.response.status(this.statusCode).json(content); - } - }; -} - -export interface SessionContext extends MiddlewareContext { - session?: SessionContainerInterface; -} - -export interface LoopbackFramework extends Framework { - middleware: Middleware; -} -export const middleware: Middleware = async (ctx: MiddlewareContext, next: Next) => { - let supertokens = SuperTokens.getInstanceOrThrowError(); - let request = new LoopbackRequest(ctx); - let response = new LoopbackResponse(ctx); - try { - let result = await supertokens.middleware(request, response); - if (!result) { - return await next(); - } - return; - } catch (err) { - return await supertokens.errorHandler(err, request, response); - } -}; - -export const LoopbackWrapper: LoopbackFramework = { - middleware, - wrapRequest: (unwrapped) => { - return new LoopbackRequest(unwrapped); - }, - wrapResponse: (unwrapped) => { - return new LoopbackResponse(unwrapped); - }, -}; diff --git a/lib/ts/framework/loopback/index.ts b/lib/ts/framework/loopback/index.ts deleted file mode 100644 index 448d6874c..000000000 --- a/lib/ts/framework/loopback/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { LoopbackWrapper } from "./framework"; -export type { SessionContext } from "./framework"; - -export const middleware = LoopbackWrapper.middleware; -export const wrapRequest = LoopbackWrapper.wrapRequest; -export const wrapResponse = LoopbackWrapper.wrapResponse; diff --git a/lib/ts/framework/request.ts b/lib/ts/framework/request.ts deleted file mode 100644 index 09d36864c..000000000 --- a/lib/ts/framework/request.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { HTTPMethod } from "../types"; - -export abstract class BaseRequest { - wrapperUsed: boolean; - original: any; - constructor() { - this.wrapperUsed = true; - } - abstract getKeyValueFromQuery: (key: string) => string | undefined; - abstract getJSONBody: () => Promise; - abstract getMethod: () => HTTPMethod; - abstract getCookieValue: (key_: string) => string | undefined; - abstract getHeaderValue: (key: string) => string | undefined; - abstract getOriginalURL: () => string; - abstract getFormData: () => Promise; -} diff --git a/lib/ts/framework/response.ts b/lib/ts/framework/response.ts deleted file mode 100644 index e7e4312f7..000000000 --- a/lib/ts/framework/response.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export abstract class BaseResponse { - wrapperUsed: boolean; - original: any; - constructor() { - this.wrapperUsed = true; - } - abstract setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; - abstract removeHeader: (key: string) => void; - abstract setCookie: ( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" - ) => void; - abstract setStatusCode: (statusCode: number) => void; - abstract sendJSONResponse: (content: any) => void; - abstract sendHTMLResponse: (html: string) => void; -} diff --git a/lib/ts/framework/types.ts b/lib/ts/framework/types.ts deleted file mode 100644 index 5707c65d5..000000000 --- a/lib/ts/framework/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export type TypeFramework = "express" | "fastify" | "hapi" | "loopback" | "koa" | "awsLambda"; -import { BaseRequest, BaseResponse } from "."; - -export let SchemaFramework = { - type: "string", - enum: ["express", "fastify", "hapi", "loopback", "koa", "awsLambda"], -}; - -export interface Framework { - wrapRequest: (unwrapped: any) => BaseRequest; - - wrapResponse: (unwrapped: any) => BaseResponse; -} diff --git a/lib/ts/framework/utils.ts b/lib/ts/framework/utils.ts deleted file mode 100644 index 455feeaba..000000000 --- a/lib/ts/framework/utils.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { parse, serialize } from "cookie"; -import type { Request, Response } from "express"; -import { json, urlencoded } from "body-parser"; -import type { IncomingMessage } from "http"; -import { ServerResponse } from "http"; -import STError from "../error"; -import type { HTTPMethod } from "../types"; -import { NextApiRequest } from "next"; -import { COOKIE_HEADER } from "./constants"; -import { getFromObjectCaseInsensitive } from "../utils"; - -export function getCookieValueFromHeaders(headers: any, key: string): string | undefined { - if (headers === undefined || headers === null) { - return undefined; - } - let cookies: any = headers.cookie || headers.Cookie; - - if (cookies === undefined) { - return undefined; - } - - cookies = parse(cookies); - - // parse JSON cookies - cookies = JSONCookies(cookies); - - return (cookies as any)[key]; -} - -export function getCookieValueFromIncomingMessage(request: IncomingMessage, key: string): string | undefined { - if ((request as any).cookies) { - return (request as any).cookies[key]; - } - - return getCookieValueFromHeaders(request.headers, key); -} - -export function getHeaderValueFromIncomingMessage(request: IncomingMessage, key: string): string | undefined { - return normalizeHeaderValue(getFromObjectCaseInsensitive(key, request.headers)); -} - -export function normalizeHeaderValue(value: string | string[] | undefined): string | undefined { - if (value === undefined) { - return undefined; - } - if (Array.isArray(value)) { - return value[0]; - } - return value; -} - -/** - * Parse JSON cookie string. - * - * @param {String} str - * @return {Object} Parsed object or undefined if not json cookie - * @public - */ - -function JSONCookie(str: string) { - if (typeof str !== "string" || str.substr(0, 2) !== "j:") { - return undefined; - } - - try { - return JSON.parse(str.slice(2)); - } catch (err) { - return undefined; - } -} - -/** - * Parse JSON cookies. - * - * @param {Object} obj - * @return {Object} - * @public - */ - -function JSONCookies(obj: any) { - let cookies = Object.keys(obj); - let key; - let val; - - for (let i = 0; i < cookies.length; i++) { - key = cookies[i]; - val = JSONCookie(obj[key]); - - if (val) { - obj[key] = val; - } - } - - return obj; -} - -export async function assertThatBodyParserHasBeenUsedForExpressLikeRequest( - method: HTTPMethod, - request: (Request | NextApiRequest) & { __supertokensFromNextJS?: true } -) { - // according to https://github.com/supertokens/supertokens-node/issues/33 - if (method === "post" || method === "put") { - if (typeof request.body === "string") { - try { - request.body = JSON.parse(request.body); - } catch (err) { - if (request.body === "") { - request.body = {}; - } else { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass a valid JSON input in the request body", - }); - } - } - } else if ( - request.body === undefined || - Buffer.isBuffer(request.body) || - Object.keys(request.body).length === 0 - ) { - // parsing it again to make sure that the request is parsed atleast once by a json parser - let jsonParser = json(); - let err = await new Promise((resolve) => { - let resolvedCalled = false; - if (request.readable) { - jsonParser(request, new ServerResponse(request), (e) => { - if (!resolvedCalled) { - resolvedCalled = true; - resolve(e); - } - }); - } else { - resolve(undefined); - } - }); - if (err !== undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass a valid JSON input in the request body", - }); - } - } - } else if (method === "delete" || method === "get") { - if (request.query === undefined) { - let parser = urlencoded({ extended: true }); - let err = await new Promise((resolve) => parser(request, new ServerResponse(request), resolve)); - if (err !== undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass valid url encoded form in the request body", - }); - } - } - } -} - -export async function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest( - request: (Request | NextApiRequest) & { __supertokensFromNextJS?: true } -) { - let parser = urlencoded({ extended: true }); - let err = await new Promise((resolve) => { - if (request.readable) { - parser(request, new ServerResponse(request), (e) => { - resolve(e); - }); - } else { - resolve(undefined); - } - }); - if (err !== undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "API input error: Please make sure to pass valid url encoded form in the request body", - }); - } -} - -export function setHeaderForExpressLikeResponse(res: Response, key: string, value: string, allowDuplicateKey: boolean) { - try { - let existingHeaders = res.getHeaders(); - let existingValue = existingHeaders[key.toLowerCase()]; - - // we have the res.header for compatibility with nextJS - if (existingValue === undefined) { - if (res.header !== undefined) { - res.header(key, value); - } else { - res.setHeader(key, value); - } - } else if (allowDuplicateKey) { - if (res.header !== undefined) { - res.header(key, existingValue + ", " + value); - } else { - res.setHeader(key, existingValue + ", " + value); - } - } else { - // we overwrite the current one with the new one - if (res.header !== undefined) { - res.header(key, value); - } else { - res.setHeader(key, value); - } - } - } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); - } -} - -/** - * - * @param res - * @param name - * @param value - * @param domain - * @param secure - * @param httpOnly - * @param expires - * @param path - */ -export function setCookieForServerResponse( - res: ServerResponse, - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" -) { - return appendToServerResponse( - res, - COOKIE_HEADER, - serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite), - key - ); -} - -/** - * Append additional header `field` with value `val`. - * - * Example: - * - * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); - * - * @param {ServerResponse} res - * @param {string} field - * @param {string| string[]} val - */ -function appendToServerResponse(res: ServerResponse, field: string, val: string | string[], key: string) { - let prev: string | string[] | undefined = res.getHeader(field) as string | string[] | undefined; - res.setHeader(field, getCookieValueToSetInHeader(prev, val, key)); - return res; -} - -export function getCookieValueToSetInHeader( - prev: string | string[] | undefined, - val: string | string[], - key: string -): string | string[] { - let value = val; - - if (prev !== undefined) { - // removing existing cookie with the same name - if (Array.isArray(prev)) { - let removedDuplicate = []; - for (let i = 0; i < prev.length; i++) { - let curr = prev[i]; - if (!curr.startsWith(key)) { - removedDuplicate.push(curr); - } - } - prev = removedDuplicate; - } else { - if (prev.startsWith(key)) { - prev = undefined; - } - } - if (prev !== undefined) { - value = Array.isArray(prev) ? prev.concat(val) : Array.isArray(val) ? [prev].concat(val) : [prev, val]; - } - } - - value = Array.isArray(value) ? value.map(String) : String(value); - return value; -} - -export function serializeCookieValue( - key: string, - value: string, - domain: string | undefined, - secure: boolean, - httpOnly: boolean, - expires: number, - path: string, - sameSite: "strict" | "lax" | "none" -): string { - let opts = { - domain, - secure, - httpOnly, - expires: new Date(expires), - path, - sameSite, - }; - - return serialize(key, value, opts); -} diff --git a/lib/ts/index.ts b/lib/ts/index.ts deleted file mode 100644 index b525a9987..000000000 --- a/lib/ts/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import SuperTokens from "./supertokens"; -import SuperTokensError from "./error"; - -// For Express -export default class SuperTokensWrapper { - static init = SuperTokens.init; - - static Error = SuperTokensError; - - static getAllCORSHeaders() { - return SuperTokens.getInstanceOrThrowError().getAllCORSHeaders(); - } - - static getUserCount(includeRecipeIds?: string[]) { - return SuperTokens.getInstanceOrThrowError().getUserCount(includeRecipeIds); - } - - static getUsersOldestFirst(input?: { - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - }): Promise<{ - users: { recipeId: string; user: any }[]; - nextPaginationToken?: string; - }> { - return SuperTokens.getInstanceOrThrowError().getUsers({ - timeJoinedOrder: "ASC", - ...input, - }); - } - - static getUsersNewestFirst(input?: { - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - }): Promise<{ - users: { recipeId: string; user: any }[]; - nextPaginationToken?: string; - }> { - return SuperTokens.getInstanceOrThrowError().getUsers({ - timeJoinedOrder: "DESC", - ...input, - }); - } - - static deleteUser(userId: string) { - return SuperTokens.getInstanceOrThrowError().deleteUser({ - userId, - }); - } - - static createUserIdMapping(input: { - superTokensUserId: string; - externalUserId: string; - externalUserIdInfo?: string; - force?: boolean; - }) { - return SuperTokens.getInstanceOrThrowError().createUserIdMapping(input); - } - - static getUserIdMapping(input: { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }) { - return SuperTokens.getInstanceOrThrowError().getUserIdMapping(input); - } - - static deleteUserIdMapping(input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - force?: boolean; - }) { - return SuperTokens.getInstanceOrThrowError().deleteUserIdMapping(input); - } - - static updateOrDeleteUserIdMappingInfo(input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - externalUserIdInfo?: string; - }) { - return SuperTokens.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo(input); - } -} - -export let init = SuperTokensWrapper.init; - -export let getAllCORSHeaders = SuperTokensWrapper.getAllCORSHeaders; - -export let getUserCount = SuperTokensWrapper.getUserCount; - -export let getUsersOldestFirst = SuperTokensWrapper.getUsersOldestFirst; - -export let getUsersNewestFirst = SuperTokensWrapper.getUsersNewestFirst; - -export let deleteUser = SuperTokensWrapper.deleteUser; - -export let createUserIdMapping = SuperTokensWrapper.createUserIdMapping; - -export let getUserIdMapping = SuperTokensWrapper.getUserIdMapping; - -export let deleteUserIdMapping = SuperTokensWrapper.deleteUserIdMapping; - -export let updateOrDeleteUserIdMappingInfo = SuperTokensWrapper.updateOrDeleteUserIdMappingInfo; - -export let Error = SuperTokensWrapper.Error; diff --git a/lib/ts/ingredients/emaildelivery/index.ts b/lib/ts/ingredients/emaildelivery/index.ts deleted file mode 100644 index e45f4654e..000000000 --- a/lib/ts/ingredients/emaildelivery/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeInputWithService, EmailDeliveryInterface } from "./types"; -import OverrideableBuilder from "supertokens-js-override"; - -export default class EmailDelivery { - ingredientInterfaceImpl: EmailDeliveryInterface; - - constructor(config: TypeInputWithService) { - let builder = new OverrideableBuilder(config.service); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.ingredientInterfaceImpl = builder.build(); - } -} diff --git a/lib/ts/ingredients/emaildelivery/services/smtp.ts b/lib/ts/ingredients/emaildelivery/services/smtp.ts deleted file mode 100644 index f35fc2aeb..000000000 --- a/lib/ts/ingredients/emaildelivery/services/smtp.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import OverrideableBuilder from "supertokens-js-override"; - -export interface SMTPServiceConfig { - host: string; - from: { - name: string; - email: string; - }; - port: number; - secure?: boolean; - authUsername?: string; - password: string; -} - -export interface GetContentResult { - body: string; - isHtml: boolean; - subject: string; - toEmail: string; -} - -export type TypeInputSendRawEmail = GetContentResult & { userContext: any }; - -export type ServiceInterface = { - sendRawEmail: (input: TypeInputSendRawEmail) => Promise; - getContent: (input: T & { userContext: any }) => Promise; -}; - -export type TypeInput = { - smtpSettings: SMTPServiceConfig; - override?: (oI: ServiceInterface, builder: OverrideableBuilder>) => ServiceInterface; -}; diff --git a/lib/ts/ingredients/emaildelivery/types.ts b/lib/ts/ingredients/emaildelivery/types.ts deleted file mode 100644 index 8dee2f95f..000000000 --- a/lib/ts/ingredients/emaildelivery/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import OverrideableBuilder from "supertokens-js-override"; - -export type EmailDeliveryInterface = { - sendEmail: (input: T & { userContext: any }) => Promise; -}; - -/** - * config class parameter when parent Recipe create a new EmailDeliveryIngredient object via constructor - */ -export interface TypeInput { - service?: EmailDeliveryInterface; - override?: ( - originalImplementation: EmailDeliveryInterface, - builder: OverrideableBuilder> - ) => EmailDeliveryInterface; -} - -export interface TypeInputWithService { - service: EmailDeliveryInterface; - override?: ( - originalImplementation: EmailDeliveryInterface, - builder: OverrideableBuilder> - ) => EmailDeliveryInterface; -} diff --git a/lib/ts/ingredients/smsdelivery/index.ts b/lib/ts/ingredients/smsdelivery/index.ts deleted file mode 100644 index e17c2cf82..000000000 --- a/lib/ts/ingredients/smsdelivery/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeInputWithService, SmsDeliveryInterface } from "./types"; -import OverrideableBuilder from "supertokens-js-override"; - -export default class SmsDelivery { - ingredientInterfaceImpl: SmsDeliveryInterface; - - constructor(config: TypeInputWithService) { - let builder = new OverrideableBuilder(config.service); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.ingredientInterfaceImpl = builder.build(); - } -} diff --git a/lib/ts/ingredients/smsdelivery/services/supertokens.ts b/lib/ts/ingredients/smsdelivery/services/supertokens.ts deleted file mode 100644 index 46b778a98..000000000 --- a/lib/ts/ingredients/smsdelivery/services/supertokens.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const SUPERTOKENS_SMS_SERVICE_URL = "https://api.supertokens.com/0/services/sms"; diff --git a/lib/ts/ingredients/smsdelivery/services/twilio.ts b/lib/ts/ingredients/smsdelivery/services/twilio.ts deleted file mode 100644 index 5ef866db3..000000000 --- a/lib/ts/ingredients/smsdelivery/services/twilio.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import OverrideableBuilder from "supertokens-js-override"; -import { ClientOpts } from "twilio/lib/base/BaseTwilio"; - -/** - * only one of "from" and "messagingServiceSid" should be passed. - * if both are passed, we should throw error to the user - * saying that only one of them should be set. this is because - * both parameters can't be passed while calling twilio API. - * if none of "from" and "messagingServiceSid" is passed, error - * should be thrown. - */ -export type TwilioServiceConfig = - | { - accountSid: string; - authToken: string; - from: string; - opts?: ClientOpts; - } - | { - accountSid: string; - authToken: string; - messagingServiceSid: string; - opts?: ClientOpts; - }; - -export interface GetContentResult { - body: string; - toPhoneNumber: string; -} - -export type TypeInputSendRawSms = GetContentResult & { userContext: any } & ( - | { - from: string; - } - | { - messagingServiceSid: string; - } - ); - -export type ServiceInterface = { - sendRawSms: (input: TypeInputSendRawSms) => Promise; - getContent: (input: T & { userContext: any }) => Promise; -}; - -export type TypeInput = { - twilioSettings: TwilioServiceConfig; - override?: (oI: ServiceInterface, builder: OverrideableBuilder>) => ServiceInterface; -}; - -export function normaliseUserInputConfig(input: TypeInput): TypeInput { - let from = "from" in input.twilioSettings ? input.twilioSettings.from : undefined; - let messagingServiceSid = - "messagingServiceSid" in input.twilioSettings ? input.twilioSettings.messagingServiceSid : undefined; - if ( - (from === undefined && messagingServiceSid === undefined) || - (from !== undefined && messagingServiceSid !== undefined) - ) { - throw Error(`Please pass exactly one of "from" and "messagingServiceSid" config for twilioSettings.`); - } - return input; -} diff --git a/lib/ts/ingredients/smsdelivery/types.ts b/lib/ts/ingredients/smsdelivery/types.ts deleted file mode 100644 index 44eb0c28b..000000000 --- a/lib/ts/ingredients/smsdelivery/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import OverrideableBuilder from "supertokens-js-override"; - -export type SmsDeliveryInterface = { - sendSms: (input: T & { userContext: any }) => Promise; -}; - -/** - * config class parameter when parent Recipe create a new SmsDeliveryIngredient object via constructor - */ -export interface TypeInput { - service?: SmsDeliveryInterface; - override?: ( - originalImplementation: SmsDeliveryInterface, - builder: OverrideableBuilder> - ) => SmsDeliveryInterface; -} - -export interface TypeInputWithService { - service: SmsDeliveryInterface; - override?: ( - originalImplementation: SmsDeliveryInterface, - builder: OverrideableBuilder> - ) => SmsDeliveryInterface; -} diff --git a/lib/ts/logger.ts b/lib/ts/logger.ts deleted file mode 100644 index 42fa1be9b..000000000 --- a/lib/ts/logger.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import debug from "debug"; -import { version } from "./version"; - -const SUPERTOKENS_DEBUG_NAMESPACE = "com.supertokens"; -/* - The debug logger below can be used to log debug messages in the following format - com.supertokens {t: "2022-03-18T11:15:24.608Z", message: Your message, file: "/home/supertokens-node/lib/build/supertokens.js:231:18" sdkVer: "9.2.0"} +0m -*/ - -function logDebugMessage(message: string) { - if (debug.enabled(SUPERTOKENS_DEBUG_NAMESPACE)) { - debug(SUPERTOKENS_DEBUG_NAMESPACE)( - `{t: "${new Date().toISOString()}", message: \"${message}\", file: \"${getFileLocation()}\" sdkVer: "${version}"}` - ); - console.log(); - } -} - -let getFileLocation = () => { - let errorObject = new Error(); - if (errorObject.stack === undefined) { - // should not come here - return "N/A"; - } - // split the error stack into an array with new line as the separator - let errorStack = errorObject.stack.split("\n"); - - // find return the first trace which doesnt have the logger.js file - for (let i = 1; i < errorStack.length; i++) { - if (!errorStack[i].includes("logger.js")) { - // retrieve the string between the parenthesis - return errorStack[i].match(/(?<=\().+?(?=\))/g); - } - } - return "N/A"; -}; - -export { logDebugMessage }; diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts deleted file mode 100644 index b09d6c623..000000000 --- a/lib/ts/nextjs.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { errorHandler } from "./framework/express"; -function next( - request: any, - response: any, - resolve: (value?: any) => void, - reject: (reason?: any) => void -): (middlewareError?: any) => Promise { - return async function (middlewareError?: any) { - if (middlewareError === undefined) { - return resolve(); - } - await errorHandler()(middlewareError, request, response, (errorHandlerError: any) => { - if (errorHandlerError !== undefined) { - return reject(errorHandlerError); - } - - // do nothing, error handler does not resolve the promise. - }); - }; -} -export default class NextJS { - static async superTokensNextWrapper( - middleware: (next: (middlewareError?: any) => void) => Promise, - request: any, - response: any - ): Promise { - return new Promise(async (resolve: any, reject: any) => { - request.__supertokensFromNextJS = true; - try { - let callbackCalled = false; - const result = await middleware((err) => { - callbackCalled = true; - next(request, response, resolve, reject)(err); - }); - if (!callbackCalled && !response.finished && !response.headersSent) { - return resolve(result); - } - } catch (err) { - await errorHandler()(err, request, response, (errorHandlerError: any) => { - if (errorHandlerError !== undefined) { - return reject(errorHandlerError); - } - // do nothing, error handler does not resolve the promise. - }); - } - }); - } -} -export let superTokensNextWrapper = NextJS.superTokensNextWrapper; diff --git a/lib/ts/normalisedURLDomain.ts b/lib/ts/normalisedURLDomain.ts deleted file mode 100644 index 4b191e8c7..000000000 --- a/lib/ts/normalisedURLDomain.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { URL } from "url"; -import { isAnIpAddress } from "./utils"; - -export default class NormalisedURLDomain { - private value: string; - - constructor(url: string) { - this.value = normaliseURLDomainOrThrowError(url); - } - - getAsStringDangerous = () => { - return this.value; - }; -} - -function normaliseURLDomainOrThrowError(input: string, ignoreProtocol = false): string { - input = input.trim().toLowerCase(); - - try { - if (!input.startsWith("http://") && !input.startsWith("https://") && !input.startsWith("supertokens://")) { - throw new Error("converting to proper URL"); - } - let urlObj = new URL(input); - if (ignoreProtocol) { - if (urlObj.hostname.startsWith("localhost") || isAnIpAddress(urlObj.hostname)) { - input = "http://" + urlObj.host; - } else { - input = "https://" + urlObj.host; - } - } else { - input = urlObj.protocol + "//" + urlObj.host; - } - - return input; - } catch (err) {} - // not a valid URL - - if (input.startsWith("/")) { - throw Error("Please provide a valid domain name"); - } - - if (input.indexOf(".") === 0) { - input = input.substr(1); - } - - // If the input contains a . it means they have given a domain name. - // So we try assuming that they have given a domain name - if ( - (input.indexOf(".") !== -1 || input.startsWith("localhost")) && - !input.startsWith("http://") && - !input.startsWith("https://") - ) { - input = "https://" + input; - - // at this point, it should be a valid URL. So we test that before doing a recursive call - try { - new URL(input); - return normaliseURLDomainOrThrowError(input, true); - } catch (err) {} - } - - throw Error("Please provide a valid domain name"); -} diff --git a/lib/ts/normalisedURLPath.ts b/lib/ts/normalisedURLPath.ts deleted file mode 100644 index 1741c73a2..000000000 --- a/lib/ts/normalisedURLPath.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { URL } from "url"; - -export default class NormalisedURLPath { - private value: string; - - constructor(url: string) { - this.value = normaliseURLPathOrThrowError(url); - } - - startsWith = (other: NormalisedURLPath) => { - return this.value.startsWith(other.value); - }; - - appendPath = (other: NormalisedURLPath) => { - return new NormalisedURLPath(this.value + other.value); - }; - - getAsStringDangerous = () => { - return this.value; - }; - - equals = (other: NormalisedURLPath) => { - return this.value === other.value; - }; - - isARecipePath = () => { - return this.value === "/recipe" || this.value.startsWith("/recipe/"); - }; -} - -function normaliseURLPathOrThrowError(input: string): string { - input = input.trim().toLowerCase(); - - try { - if (!input.startsWith("http://") && !input.startsWith("https://")) { - throw new Error("converting to proper URL"); - } - let urlObj = new URL(input); - input = urlObj.pathname; - - if (input.charAt(input.length - 1) === "/") { - return input.substr(0, input.length - 1); - } - - return input; - } catch (err) {} - // not a valid URL - - // If the input contains a . it means they have given a domain name. - // So we try assuming that they have given a domain name + path - if ( - (domainGiven(input) || input.startsWith("localhost")) && - !input.startsWith("http://") && - !input.startsWith("https://") - ) { - input = "http://" + input; - return normaliseURLPathOrThrowError(input); - } - - if (input.charAt(0) !== "/") { - input = "/" + input; - } - - // at this point, we should be able to convert it into a fake URL and recursively call this function. - try { - // test that we can convert this to prevent an infinite loop - new URL("http://example.com" + input); - - return normaliseURLPathOrThrowError("http://example.com" + input); - } catch (err) { - throw Error("Please provide a valid URL path"); - } -} - -function domainGiven(input: string): boolean { - // If no dot, return false. - if (input.indexOf(".") === -1 || input.startsWith("/")) { - return false; - } - - try { - let url = new URL(input); - return url.hostname.indexOf(".") !== -1; - } catch (ignored) {} - - try { - let url = new URL("http://" + input); - return url.hostname.indexOf(".") !== -1; - } catch (ignored) {} - - return false; -} diff --git a/lib/ts/postSuperTokensInitCallbacks.ts b/lib/ts/postSuperTokensInitCallbacks.ts deleted file mode 100644 index 403e6e135..000000000 --- a/lib/ts/postSuperTokensInitCallbacks.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export class PostSuperTokensInitCallbacks { - static postInitCallbacks: (() => void)[] = []; - - static addPostInitCallback(cb: () => void) { - PostSuperTokensInitCallbacks.postInitCallbacks.push(cb); - } - - static runPostInitCallbacks() { - for (const cb of PostSuperTokensInitCallbacks.postInitCallbacks) { - cb(); - } - PostSuperTokensInitCallbacks.postInitCallbacks = []; - } -} diff --git a/lib/ts/processState.ts b/lib/ts/processState.ts deleted file mode 100644 index 05b9c4de0..000000000 --- a/lib/ts/processState.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export enum PROCESS_STATE { - CALLING_SERVICE_IN_VERIFY, - CALLING_SERVICE_IN_GET_HANDSHAKE_INFO, - CALLING_SERVICE_IN_GET_API_VERSION, - CALLING_SERVICE_IN_REQUEST_HELPER, -} - -export class ProcessState { - history: PROCESS_STATE[] = []; - private static instance: ProcessState | undefined; - - private constructor() {} - - static getInstance() { - if (ProcessState.instance === undefined) { - ProcessState.instance = new ProcessState(); - } - return ProcessState.instance; - } - - addState = (state: PROCESS_STATE) => { - if (process.env.TEST_MODE === "testing") { - this.history.push(state); - } - }; - - private getEventByLastEventByName = (state: PROCESS_STATE) => { - for (let i = this.history.length - 1; i >= 0; i--) { - if (this.history[i] === state) { - return this.history[i]; - } - } - return undefined; - }; - - reset = () => { - this.history = []; - }; - - waitForEvent = async (state: PROCESS_STATE, timeInMS = 7000) => { - let startTime = Date.now(); - return new Promise((resolve) => { - let actualThis = this; - function tryAndGet() { - let result = actualThis.getEventByLastEventByName(state); - if (result === undefined) { - if (Date.now() - startTime > timeInMS) { - resolve(undefined); - } else { - setTimeout(tryAndGet, 1000); - } - } else { - resolve(result); - } - } - tryAndGet(); - }); - }; -} diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts deleted file mode 100644 index 85f759f45..000000000 --- a/lib/ts/querier.ts +++ /dev/null @@ -1,285 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import axios from "axios"; - -import { getLargestVersionFromIntersection } from "./utils"; -import { cdiSupported } from "./version"; -import NormalisedURLDomain from "./normalisedURLDomain"; -import NormalisedURLPath from "./normalisedURLPath"; -import { PROCESS_STATE, ProcessState } from "./processState"; - -export class Querier { - private static initCalled = false; - private static hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined = undefined; - private static apiKey: string | undefined = undefined; - private static apiVersion: string | undefined = undefined; - - private static lastTriedIndex = 0; - private static hostsAliveForTesting: Set = new Set(); - - private __hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined; - private rIdToCore: string | undefined; - - // we have rIdToCore so that recipes can force change the rId sent to core. This is a hack until the core is able - // to support multiple rIds per API - private constructor( - hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined, - rIdToCore?: string - ) { - this.__hosts = hosts; - this.rIdToCore = rIdToCore; - } - - getAPIVersion = async (): Promise => { - if (Querier.apiVersion !== undefined) { - return Querier.apiVersion; - } - ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION); - let response = await this.sendRequestHelper( - new NormalisedURLPath("/apiversion"), - "GET", - (url: string) => { - let headers: any = {}; - if (Querier.apiKey !== undefined) { - headers = { - "api-key": Querier.apiKey, - }; - } - return axios.get(url, { - headers, - }); - }, - this.__hosts?.length || 0 - ); - let cdiSupportedByServer: string[] = response.versions; - let supportedVersion = getLargestVersionFromIntersection(cdiSupportedByServer, cdiSupported); - if (supportedVersion === undefined) { - throw Error( - "The running SuperTokens core version is not compatible with this NodeJS SDK. Please visit https://supertokens.io/docs/community/compatibility to find the right versions" - ); - } - Querier.apiVersion = supportedVersion; - return Querier.apiVersion; - }; - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw Error("calling testing function in non testing env"); - } - Querier.initCalled = false; - } - - getHostsAliveForTesting = () => { - if (process.env.TEST_MODE !== "testing") { - throw Error("calling testing function in non testing env"); - } - return Querier.hostsAliveForTesting; - }; - - static getNewInstanceOrThrowError(rIdToCore?: string): Querier { - if (!Querier.initCalled) { - throw Error("Please call the supertokens.init function before using SuperTokens"); - } - return new Querier(Querier.hosts, rIdToCore); - } - - static init(hosts?: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[], apiKey?: string) { - if (!Querier.initCalled) { - Querier.initCalled = true; - Querier.hosts = hosts; - Querier.apiKey = apiKey; - Querier.apiVersion = undefined; - Querier.lastTriedIndex = 0; - Querier.hostsAliveForTesting = new Set(); - } - } - - // path should start with "/" - sendPostRequest = async (path: NormalisedURLPath, body: any): Promise => { - return this.sendRequestHelper( - path, - "POST", - async (url: string) => { - let apiVersion = await this.getAPIVersion(); - let headers: any = { - "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", - }; - if (Querier.apiKey !== undefined) { - headers = { - ...headers, - "api-key": Querier.apiKey, - }; - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = { - ...headers, - rid: this.rIdToCore, - }; - } - return await axios({ - method: "POST", - url, - data: body, - headers, - }); - }, - this.__hosts?.length || 0 - ); - }; - - // path should start with "/" - sendDeleteRequest = async (path: NormalisedURLPath, body: any, params?: any): Promise => { - return this.sendRequestHelper( - path, - "DELETE", - async (url: string) => { - let apiVersion = await this.getAPIVersion(); - let headers: any = { "cdi-version": apiVersion }; - if (Querier.apiKey !== undefined) { - headers = { - ...headers, - "api-key": Querier.apiKey, - "content-type": "application/json; charset=utf-8", - }; - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = { - ...headers, - rid: this.rIdToCore, - }; - } - return await axios({ - method: "DELETE", - url, - data: body, - headers, - params, - }); - }, - this.__hosts?.length || 0 - ); - }; - - // path should start with "/" - sendGetRequest = async (path: NormalisedURLPath, params: any): Promise => { - return this.sendRequestHelper( - path, - "GET", - async (url: string) => { - let apiVersion = await this.getAPIVersion(); - let headers: any = { "cdi-version": apiVersion }; - if (Querier.apiKey !== undefined) { - headers = { - ...headers, - "api-key": Querier.apiKey, - }; - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = { - ...headers, - rid: this.rIdToCore, - }; - } - return await axios.get(url, { - params, - headers, - }); - }, - this.__hosts?.length || 0 - ); - }; - - // path should start with "/" - sendPutRequest = async (path: NormalisedURLPath, body: any): Promise => { - return this.sendRequestHelper( - path, - "PUT", - async (url: string) => { - let apiVersion = await this.getAPIVersion(); - let headers: any = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" }; - if (Querier.apiKey !== undefined) { - headers = { - ...headers, - "api-key": Querier.apiKey, - }; - } - if (path.isARecipePath() && this.rIdToCore !== undefined) { - headers = { - ...headers, - rid: this.rIdToCore, - }; - } - return await axios({ - method: "PUT", - url, - data: body, - headers, - }); - }, - this.__hosts?.length || 0 - ); - }; - - // path should start with "/" - private sendRequestHelper = async ( - path: NormalisedURLPath, - method: string, - axiosFunction: (url: string) => Promise, - numberOfTries: number - ): Promise => { - if (this.__hosts === undefined) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - if (numberOfTries === 0) { - throw Error("No SuperTokens core available to query"); - } - let currentDomain: string = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); - let currentBasePath: string = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); - Querier.lastTriedIndex++; - Querier.lastTriedIndex = Querier.lastTriedIndex % this.__hosts.length; - try { - ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER); - let response = await axiosFunction(currentDomain + currentBasePath + path.getAsStringDangerous()); - if (process.env.TEST_MODE === "testing") { - Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); - } - if (response.status !== 200) { - throw response; - } - return response.data; - } catch (err) { - if (err.message !== undefined && err.message.includes("ECONNREFUSED")) { - return await this.sendRequestHelper(path, method, axiosFunction, numberOfTries - 1); - } - if (err.response !== undefined && err.response.status !== undefined && err.response.data !== undefined) { - throw new Error( - "SuperTokens core threw an error for a " + - method + - " request to path: '" + - path.getAsStringDangerous() + - "' with status code: " + - err.response.status + - " and message: " + - err.response.data - ); - } else { - throw err; - } - } - }; -} diff --git a/lib/ts/recipe/dashboard/api/analytics.ts b/lib/ts/recipe/dashboard/api/analytics.ts deleted file mode 100644 index 07df39b36..000000000 --- a/lib/ts/recipe/dashboard/api/analytics.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { APIInterface, APIOptions } from "../types"; -import SuperTokens from "../../../supertokens"; -import { Querier } from "../../../querier"; -import NormalisedURLPath from "../../../normalisedURLPath"; -import { version as SDKVersion } from "../../../version"; -import STError from "../../../error"; -import axios from "axios"; - -export type Response = { - status: "OK"; -}; - -export default async function analyticsPost(_: APIInterface, options: APIOptions): Promise { - // If telemetry is disabled, dont send any event - if (!SuperTokens.getInstanceOrThrowError().telemetryEnabled) { - return { - status: "OK", - }; - } - - const { email, dashboardVersion } = await options.req.getJSONBody(); - - if (email === undefined) { - throw new STError({ - message: "Missing required property 'email'", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (dashboardVersion === undefined) { - throw new STError({ - message: "Missing required property 'dashboardVersion'", - type: STError.BAD_INPUT_ERROR, - }); - } - - let telemetryId: string | undefined; - let numberOfUsers: number; - try { - let querier = Querier.getNewInstanceOrThrowError(options.recipeId); - let response = await querier.sendGetRequest(new NormalisedURLPath("/telemetry"), {}); - if (response.exists) { - telemetryId = response.telemetryId; - } - - numberOfUsers = await SuperTokens.getInstanceOrThrowError().getUserCount(); - } catch (_) { - // If either telemetry id API or user count fetch fails, no event should be sent - return { - status: "OK", - }; - } - - const { apiDomain, websiteDomain, appName } = options.appInfo; - const data = { - websiteDomain: websiteDomain.getAsStringDangerous(), - apiDomain: apiDomain.getAsStringDangerous(), - appName, - sdk: "node", - sdkVersion: SDKVersion, - telemetryId, - numberOfUsers, - email, - dashboardVersion, - }; - - try { - await axios({ - url: "https://api.supertokens.com/0/st/telemetry", - method: "POST", - data, - headers: { - "api-version": 3, - }, - }); - } catch (e) { - // Ignored - } - - return { - status: "OK", - }; -} diff --git a/lib/ts/recipe/dashboard/api/apiKeyProtector.ts b/lib/ts/recipe/dashboard/api/apiKeyProtector.ts deleted file mode 100644 index bcdaa6d47..000000000 --- a/lib/ts/recipe/dashboard/api/apiKeyProtector.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { makeDefaultUserContextFromAPI } from "../../../utils"; -import { APIFunction, APIInterface, APIOptions } from "../types"; -import { sendUnauthorisedAccess } from "../utils"; - -export default async function apiKeyProtector( - apiImplementation: APIInterface, - options: APIOptions, - apiFunction: APIFunction -): Promise { - const shouldAllowAccess = await options.recipeImplementation.shouldAllowAccess({ - req: options.req, - config: options.config, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - if (!shouldAllowAccess) { - sendUnauthorisedAccess(options.res); - return true; - } - - const response = await apiFunction(apiImplementation, options); - options.res.sendJSONResponse(response); - return true; -} diff --git a/lib/ts/recipe/dashboard/api/dashboard.ts b/lib/ts/recipe/dashboard/api/dashboard.ts deleted file mode 100644 index 620802f99..000000000 --- a/lib/ts/recipe/dashboard/api/dashboard.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { makeDefaultUserContextFromAPI } from "../../../utils"; -import { APIInterface, APIOptions } from "../types"; - -export default async function dashboard(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.dashboardGET === undefined) { - return false; - } - - const htmlString = await apiImplementation.dashboardGET({ - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - options.res.sendHTMLResponse(htmlString); - - return true; -} diff --git a/lib/ts/recipe/dashboard/api/implementation.ts b/lib/ts/recipe/dashboard/api/implementation.ts deleted file mode 100644 index 39adfc15c..000000000 --- a/lib/ts/recipe/dashboard/api/implementation.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import NormalisedURLDomain from "../../../normalisedURLDomain"; -import NormalisedURLPath from "../../../normalisedURLPath"; -import { Querier } from "../../../querier"; -import SuperTokens from "../../../supertokens"; -import { maxVersion } from "../../../utils"; -import { DASHBOARD_API } from "../constants"; -import { APIInterface, AuthMode } from "../types"; - -export default function getAPIImplementation(): APIInterface { - return { - dashboardGET: async function (input) { - const bundleBasePathString = await input.options.recipeImplementation.getDashboardBundleLocation({ - userContext: input.userContext, - }); - - const bundleDomain = - new NormalisedURLDomain(bundleBasePathString).getAsStringDangerous() + - new NormalisedURLPath(bundleBasePathString).getAsStringDangerous(); - - let connectionURI: string = ""; - const superTokensInstance = SuperTokens.getInstanceOrThrowError(); - - const authMode: AuthMode = input.options.config.authMode; - - if (superTokensInstance.supertokens !== undefined) { - connectionURI = superTokensInstance.supertokens.connectionURI; - } - - let isSearchEnabled = false; - const cdiVersion = await Querier.getNewInstanceOrThrowError(input.options.recipeId).getAPIVersion(); - if (maxVersion("2.20", cdiVersion) === cdiVersion) { - // Only enable search if CDI version is 2.20 or above - isSearchEnabled = true; - } - - return ` - - - - - - - - - - -
- - - `; - }, - }; -} diff --git a/lib/ts/recipe/dashboard/api/search/tagsGet.ts b/lib/ts/recipe/dashboard/api/search/tagsGet.ts deleted file mode 100644 index 945c9eeae..000000000 --- a/lib/ts/recipe/dashboard/api/search/tagsGet.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { APIInterface, APIOptions } from "../../types"; -import { Querier } from "../../../../querier"; -import NormalisedURLPath from "../../../../normalisedURLPath"; - -type TagsResponse = { status: "OK"; tags: string[] }; - -export const getSearchTags = async (_: APIInterface, options: APIOptions): Promise => { - let querier = Querier.getNewInstanceOrThrowError(options.recipeId); - let tagsResponse = await querier.sendGetRequest(new NormalisedURLPath("/user/search/tags"), {}); - return tagsResponse; -}; diff --git a/lib/ts/recipe/dashboard/api/signIn.ts b/lib/ts/recipe/dashboard/api/signIn.ts deleted file mode 100644 index 8ea23793a..000000000 --- a/lib/ts/recipe/dashboard/api/signIn.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { APIInterface, APIOptions } from "../types"; -import { send200Response } from "../../../utils"; -import STError from "../../../error"; -import { Querier } from "../../../querier"; -import NormalisedURLPath from "../../../normalisedURLPath"; - -type SignInResponse = - | { status: "OK"; sessionId: string } - | { status: "INVALID_CREDENTIALS_ERROR" } - | { status: "USER_SUSPENDED_ERROR" }; - -export default async function signIn(_: APIInterface, options: APIOptions): Promise { - const { email, password } = await options.req.getJSONBody(); - - if (email === undefined) { - throw new STError({ - message: "Missing required parameter 'email'", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (password === undefined) { - throw new STError({ - message: "Missing required parameter 'password'", - type: STError.BAD_INPUT_ERROR, - }); - } - - let querier = Querier.getNewInstanceOrThrowError(undefined); - const signInResponse = await querier.sendPostRequest( - new NormalisedURLPath("/recipe/dashboard/signin"), - { - email, - password, - } - ); - - send200Response(options.res, signInResponse); - - return true; -} diff --git a/lib/ts/recipe/dashboard/api/signOut.ts b/lib/ts/recipe/dashboard/api/signOut.ts deleted file mode 100644 index 5fe3a0b64..000000000 --- a/lib/ts/recipe/dashboard/api/signOut.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { APIInterface, APIOptions } from "../types"; -import { send200Response } from "../../../utils"; -import { Querier } from "../../../querier"; -import NormalisedURLPath from "../../../normalisedURLPath"; - -export default async function signOut(_: APIInterface, options: APIOptions): Promise { - if (options.config.authMode === "api-key") { - send200Response(options.res, { status: "OK" }); - } else { - const sessionIdFormAuthHeader = options.req.getHeaderValue("authorization")?.split(" ")[1]; - let querier = Querier.getNewInstanceOrThrowError(undefined); - const sessionDeleteResponse = await querier.sendDeleteRequest( - new NormalisedURLPath("/recipe/dashboard/session"), - {}, - { sessionId: sessionIdFormAuthHeader } - ); - send200Response(options.res, sessionDeleteResponse); - } - return true; -} diff --git a/lib/ts/recipe/dashboard/api/userdetails/userDelete.ts b/lib/ts/recipe/dashboard/api/userdetails/userDelete.ts deleted file mode 100644 index de5af3425..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userDelete.ts +++ /dev/null @@ -1,26 +0,0 @@ -import SuperTokens from "../../../../supertokens"; -import { APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; - -type Response = { - status: "OK"; -}; - -export const userDelete = async (_: APIInterface, options: APIOptions): Promise => { - const userId = options.req.getKeyValueFromQuery("userId"); - - if (userId === undefined) { - throw new STError({ - message: "Missing required parameter 'userId'", - type: STError.BAD_INPUT_ERROR, - }); - } - - await SuperTokens.getInstanceOrThrowError().deleteUser({ - userId, - }); - - return { - status: "OK", - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts deleted file mode 100644 index bed4558ff..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { APIFunction, APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import EmailVerificationRecipe from "../../../emailverification/recipe"; -import EmailVerification from "../../../emailverification"; - -type Response = - | { - status: "OK"; - isVerified: boolean; - } - | { - status: "FEATURE_NOT_ENABLED_ERROR"; - }; - -export const userEmailverifyGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { - const req = options.req; - const userId = req.getKeyValueFromQuery("userId"); - - if (userId === undefined) { - throw new STError({ - message: "Missing required parameter 'userId'", - type: STError.BAD_INPUT_ERROR, - }); - } - - try { - EmailVerificationRecipe.getInstanceOrThrowError(); - } catch (e) { - return { - status: "FEATURE_NOT_ENABLED_ERROR", - }; - } - - const response = await EmailVerification.isEmailVerified(userId); - return { - status: "OK", - isVerified: response, - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts deleted file mode 100644 index 07bc58670..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import EmailVerification from "../../../emailverification"; - -type Response = { - status: "OK"; -}; - -export const userEmailVerifyPut = async (_: APIInterface, options: APIOptions): Promise => { - const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; - const verified = requestBody.verified; - - if (userId === undefined || typeof userId !== "string") { - throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (verified === undefined || typeof verified !== "boolean") { - throw new STError({ - message: "Required parameter 'verified' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (verified) { - const tokenResponse = await EmailVerification.createEmailVerificationToken(userId); - - if (tokenResponse.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - return { - status: "OK", - }; - } - - const verifyResponse = await EmailVerification.verifyEmailUsingToken(tokenResponse.token); - - if (verifyResponse.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { - // This should never happen because we consume the token immediately after creating it - throw new Error("Should not come here"); - } - } else { - await EmailVerification.unverifyEmail(userId); - } - - return { - status: "OK", - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts deleted file mode 100644 index 9ba03321d..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import EmailVerification from "../../../emailverification"; -import EmailVerificationRecipe from "../../../emailverification/recipe"; -import { getEmailVerifyLink } from "../../../emailverification/utils"; - -type Response = { - status: "OK" | "EMAIL_ALREADY_VERIFIED_ERROR"; -}; - -export const userEmailVerifyTokenPost = async (_: APIInterface, options: APIOptions): Promise => { - const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; - - if (userId === undefined || typeof userId !== "string") { - throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - let emailResponse = await EmailVerificationRecipe.getInstanceOrThrowError().getEmailForUserId(userId, {}); - - if (emailResponse.status !== "OK") { - throw new Error("Should never come here"); - } - - let emailVerificationToken = await EmailVerification.createEmailVerificationToken(userId); - - if (emailVerificationToken.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } - - let emailVerifyLink = getEmailVerifyLink({ - appInfo: options.appInfo, - token: emailVerificationToken.token, - recipeId: EmailVerificationRecipe.RECIPE_ID, - }); - - await EmailVerification.sendEmail({ - type: "EMAIL_VERIFICATION", - user: { - id: userId, - email: emailResponse.email, - }, - emailVerifyLink, - }); - - return { - status: "OK", - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userGet.ts deleted file mode 100644 index d11baa056..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - APIFunction, - APIInterface, - APIOptions, - EmailPasswordUser, - PasswordlessUser, - ThirdPartyUser, -} from "../../types"; -import STError from "../../../../error"; -import { getUserForRecipeId, isRecipeInitialised, isValidRecipeId } from "../../utils"; -import UserMetaDataRecipe from "../../../usermetadata/recipe"; -import UserMetaData from "../../../usermetadata"; - -type Response = - | { - status: "NO_USER_FOUND_ERROR"; - } - | { - status: "RECIPE_NOT_INITIALISED"; - } - | { - status: "OK"; - recipeId: "emailpassword"; - user: EmailPasswordUser; - } - | { - status: "OK"; - recipeId: "thirdparty"; - user: ThirdPartyUser; - } - | { - status: "OK"; - recipeId: "passwordless"; - user: PasswordlessUser; - }; - -export const userGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { - const userId = options.req.getKeyValueFromQuery("userId"); - const recipeId = options.req.getKeyValueFromQuery("recipeId"); - - if (userId === undefined) { - throw new STError({ - message: "Missing required parameter 'userId'", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (recipeId === undefined) { - throw new STError({ - message: "Missing required parameter 'recipeId'", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (!isValidRecipeId(recipeId)) { - throw new STError({ - message: "Invalid recipe id", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (!isRecipeInitialised(recipeId)) { - return { - status: "RECIPE_NOT_INITIALISED", - }; - } - - let user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined = ( - await getUserForRecipeId(userId, recipeId) - ).user; - - if (user === undefined) { - return { - status: "NO_USER_FOUND_ERROR", - }; - } - - try { - UserMetaDataRecipe.getInstanceOrThrowError(); - } catch (_) { - user = { - ...user, - firstName: "FEATURE_NOT_ENABLED", - lastName: "FEATURE_NOT_ENABLED", - }; - - return { - status: "OK", - recipeId: recipeId as any, - user, - }; - } - - const userMetaData = await UserMetaData.getUserMetadata(userId); - const { first_name, last_name } = userMetaData.metadata; - - user = { - ...user, - firstName: first_name === undefined ? "" : first_name, - lastName: last_name === undefined ? "" : last_name, - }; - - return { - status: "OK", - recipeId: recipeId as any, - user, - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts deleted file mode 100644 index 069187ac2..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { APIFunction, APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import UserMetaDataRecipe from "../../../usermetadata/recipe"; -import UserMetaData from "../../../usermetadata"; - -type Response = - | { - status: "FEATURE_NOT_ENABLED_ERROR"; - } - | { - status: "OK"; - data: any; - }; - -export const userMetaDataGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { - const userId = options.req.getKeyValueFromQuery("userId"); - - if (userId === undefined) { - throw new STError({ - message: "Missing required parameter 'userId'", - type: STError.BAD_INPUT_ERROR, - }); - } - - try { - UserMetaDataRecipe.getInstanceOrThrowError(); - } catch (e) { - return { - status: "FEATURE_NOT_ENABLED_ERROR", - }; - } - - const metaDataResponse = UserMetaData.getUserMetadata(userId); - return { - status: "OK", - data: (await metaDataResponse).metadata, - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userMetadataPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userMetadataPut.ts deleted file mode 100644 index 4f50b0012..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userMetadataPut.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { APIInterface, APIOptions } from "../../types"; -import UserMetadaRecipe from "../../../usermetadata/recipe"; -import UserMetaData from "../../../usermetadata"; -import STError from "../../../../error"; - -type Response = { - status: "OK"; -}; - -export const userMetadataPut = async (_: APIInterface, options: APIOptions): Promise => { - const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; - const data = requestBody.data; - - // This is to throw an error early in case the recipe has not been initialised - UserMetadaRecipe.getInstanceOrThrowError(); - - if (userId === undefined || typeof userId !== "string") { - throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (data === undefined || typeof data !== "string") { - throw new STError({ - message: "Required parameter 'data' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - // Make sure that data is a valid JSON, this will throw - try { - let parsedData = JSON.parse(data); - - if (typeof parsedData !== "object") { - throw new Error(); - } - - if (Array.isArray(parsedData)) { - throw new Error(); - } - - if (parsedData === null) { - throw new Error(); - } - } catch (e) { - throw new STError({ - message: "'data' must be a valid JSON body", - type: STError.BAD_INPUT_ERROR, - }); - } - - /** - * This API is meant to set the user metadata of a user. We delete the existing data - * before updating it because we want to make sure that shallow merging does not result - * in the data being incorrect - * - * For example if the old data is {test: "test", test2: "test2"} and the user wants to delete - * test2 from the data simply calling updateUserMetadata with {test: "test"} would not remove - * test2 because of shallow merging. - * - * Removing first ensures that the final data is exactly what the user wanted it to be - */ - await UserMetaData.clearUserMetadata(userId); - await UserMetaData.updateUserMetadata(userId, JSON.parse(data)); - - return { - status: "OK", - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts deleted file mode 100644 index 0a58a4306..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import EmailPasswordRecipe from "../../../emailpassword/recipe"; -import EmailPassword from "../../../emailpassword"; -import ThirdPartyEmailPasswordRecipe from "../../../thirdpartyemailpassword/recipe"; -import ThirdPartyEmailPassword from "../../../thirdpartyemailpassword"; -import { FORM_FIELD_PASSWORD_ID } from "../../../emailpassword/constants"; - -type Response = - | { - status: "OK"; - } - | { - status: "INVALID_PASSWORD_ERROR"; - error: string; - }; - -export const userPasswordPut = async (_: APIInterface, options: APIOptions): Promise => { - const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; - const newPassword = requestBody.newPassword; - - if (userId === undefined || typeof userId !== "string") { - throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (newPassword === undefined || typeof newPassword !== "string") { - throw new STError({ - message: "Required parameter 'newPassword' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - let recipeToUse: "emailpassword" | "thirdpartyemailpassword" | undefined; - - try { - EmailPasswordRecipe.getInstanceOrThrowError(); - recipeToUse = "emailpassword"; - } catch (_) {} - - if (recipeToUse === undefined) { - try { - ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - recipeToUse = "thirdpartyemailpassword"; - } catch (_) {} - } - - if (recipeToUse === undefined) { - // This means that neither emailpassword or thirdpartyemailpassword is initialised - throw new Error("Should never come here"); - } - - if (recipeToUse === "emailpassword") { - let passwordFormFields = EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_PASSWORD_ID - ); - - let passwordValidationError = await passwordFormFields[0].validate(newPassword); - - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - - const passwordResetToken = await EmailPassword.createResetPasswordToken(userId); - - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - - const passwordResetResponse = await EmailPassword.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - - return { - status: "OK", - }; - } - - let passwordFormFields = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_PASSWORD_ID - ); - - let passwordValidationError = await passwordFormFields[0].validate(newPassword); - - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - - const passwordResetToken = await ThirdPartyEmailPassword.createResetPasswordToken(userId); - - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { - // Techincally it can but its an edge case so we assume that it wont - throw new Error("Should never come here"); - } - - const passwordResetResponse = await ThirdPartyEmailPassword.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - - return { - status: "OK", - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userPut.ts deleted file mode 100644 index 10240311a..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userPut.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import EmailPasswordRecipe from "../../../emailpassword/recipe"; -import ThirdPartyEmailPasswordRecipe from "../../../thirdpartyemailpassword/recipe"; -import PasswordlessRecipe from "../../../passwordless/recipe"; -import ThirdPartyPasswordlessRecipe from "../../../thirdpartypasswordless/recipe"; -import EmailPassword from "../../../emailpassword"; -import Passwordless from "../../../passwordless"; -import ThirdPartyEmailPassword from "../../../thirdpartyemailpassword"; -import ThirdPartyPasswordless from "../../../thirdpartypasswordless"; -import { isValidRecipeId, getUserForRecipeId } from "../../utils"; -import UserMetadataRecipe from "../../../usermetadata/recipe"; -import UserMetadata from "../../../usermetadata"; -import { FORM_FIELD_EMAIL_ID } from "../../../emailpassword/constants"; -import { defaultValidateEmail, defaultValidatePhoneNumber } from "../../../passwordless/utils"; - -type Response = - | { - status: "OK"; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | { - status: "INVALID_EMAIL_ERROR"; - error: string; - } - | { - status: "PHONE_ALREADY_EXISTS_ERROR"; - } - | { - status: "INVALID_PHONE_ERROR"; - error: string; - }; - -const updateEmailForRecipeId = async ( - recipeId: "emailpassword" | "thirdparty" | "passwordless" | "thirdpartyemailpassword" | "thirdpartypasswordless", - userId: string, - email: string -): Promise< - | { - status: "OK"; - } - | { - status: "INVALID_EMAIL_ERROR"; - error: string; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } -> => { - if (recipeId === "emailpassword") { - let emailFormFields = EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_EMAIL_ID - ); - - let validationError = await emailFormFields[0].validate(email); - - if (validationError !== undefined) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - - const emailUpdateResponse = await EmailPassword.updateEmailOrPassword({ - userId, - email, - }); - - if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - - return { - status: "OK", - }; - } - - if (recipeId === "thirdpartyemailpassword") { - let emailFormFields = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_EMAIL_ID - ); - - let validationError = await emailFormFields[0].validate(email); - - if (validationError !== undefined) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - - const emailUpdateResponse = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId, - email, - }); - - if (emailUpdateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - - if (emailUpdateResponse.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - - return { - status: "OK", - }; - } - - if (recipeId === "passwordless") { - let isValidEmail = true; - let validationError = ""; - - const passwordlessConfig = PasswordlessRecipe.getInstanceOrThrowError().config; - - if (passwordlessConfig.contactMethod === "PHONE") { - const validationResult = await defaultValidateEmail(email); - - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } else { - const validationResult = await passwordlessConfig.validateEmailAddress(email); - - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } - - if (!isValidEmail) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - - const updateResult = await Passwordless.updateUser({ - userId, - email, - }); - - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - - if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - - return { - status: "OK", - }; - } - - if (recipeId === "thirdpartypasswordless") { - let isValidEmail = true; - let validationError = ""; - - const passwordlessConfig = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().passwordlessRecipe.config; - - if (passwordlessConfig.contactMethod === "PHONE") { - const validationResult = await defaultValidateEmail(email); - - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } else { - const validationResult = await passwordlessConfig.validateEmailAddress(email); - - if (validationResult !== undefined) { - isValidEmail = false; - validationError = validationResult; - } - } - - if (!isValidEmail) { - return { - status: "INVALID_EMAIL_ERROR", - error: validationError, - }; - } - - const updateResult = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId, - email, - }); - - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - - if (updateResult.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - - return { - status: "OK", - }; - } - - /** - * If it comes here then the user is a third party user in which case the UI should not have allowed this - */ - throw new Error("Should never come here"); -}; - -const updatePhoneForRecipeId = async ( - recipeId: "emailpassword" | "thirdparty" | "passwordless" | "thirdpartyemailpassword" | "thirdpartypasswordless", - userId: string, - phone: string -): Promise< - | { - status: "OK"; - } - | { - status: "INVALID_PHONE_ERROR"; - error: string; - } - | { - status: "PHONE_ALREADY_EXISTS_ERROR"; - } -> => { - if (recipeId === "passwordless") { - let isValidPhone = true; - let validationError = ""; - - const passwordlessConfig = PasswordlessRecipe.getInstanceOrThrowError().config; - - if (passwordlessConfig.contactMethod === "EMAIL") { - const validationResult = await defaultValidatePhoneNumber(phone); - - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } else { - const validationResult = await passwordlessConfig.validatePhoneNumber(phone); - - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } - - if (!isValidPhone) { - return { - status: "INVALID_PHONE_ERROR", - error: validationError, - }; - } - - const updateResult = await Passwordless.updateUser({ - userId, - phoneNumber: phone, - }); - - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - - if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { - return { - status: "PHONE_ALREADY_EXISTS_ERROR", - }; - } - - return { - status: "OK", - }; - } - - if (recipeId === "thirdpartypasswordless") { - let isValidPhone = true; - let validationError = ""; - - const passwordlessConfig = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().passwordlessRecipe.config; - - if (passwordlessConfig.contactMethod === "EMAIL") { - const validationResult = await defaultValidatePhoneNumber(phone); - - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } else { - const validationResult = await passwordlessConfig.validatePhoneNumber(phone); - - if (validationResult !== undefined) { - isValidPhone = false; - validationError = validationResult; - } - } - - if (!isValidPhone) { - return { - status: "INVALID_PHONE_ERROR", - error: validationError, - }; - } - - const updateResult = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId, - phoneNumber: phone, - }); - - if (updateResult.status === "UNKNOWN_USER_ID_ERROR") { - throw new Error("Should never come here"); - } - - if (updateResult.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { - return { - status: "PHONE_ALREADY_EXISTS_ERROR", - }; - } - - return { - status: "OK", - }; - } - - /** - * If it comes here then the user is a not a passwordless user in which case the UI should not have allowed this - */ - throw new Error("Should never come here"); -}; - -export const userPut = async (_: APIInterface, options: APIOptions): Promise => { - const requestBody = await options.req.getJSONBody(); - const userId = requestBody.userId; - const recipeId = requestBody.recipeId; - const firstName = requestBody.firstName; - const lastName = requestBody.lastName; - const email = requestBody.email; - const phone = requestBody.phone; - - if (userId === undefined || typeof userId !== "string") { - throw new STError({ - message: "Required parameter 'userId' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (recipeId === undefined || typeof recipeId !== "string") { - throw new STError({ - message: "Required parameter 'recipeId' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (!isValidRecipeId(recipeId)) { - throw new STError({ - message: "Invalid recipe id", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (firstName === undefined || typeof firstName !== "string") { - throw new STError({ - message: "Required parameter 'firstName' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (lastName === undefined || typeof lastName !== "string") { - throw new STError({ - message: "Required parameter 'lastName' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (email === undefined || typeof email !== "string") { - throw new STError({ - message: "Required parameter 'email' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - if (phone === undefined || typeof phone !== "string") { - throw new STError({ - message: "Required parameter 'phone' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - let userResponse = await getUserForRecipeId(userId, recipeId); - - if (userResponse.user === undefined || userResponse.recipe === undefined) { - throw new Error("Should never come here"); - } - - if (firstName.trim() !== "" || lastName.trim() !== "") { - let isRecipeInitialised = false; - try { - UserMetadataRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) { - // no op - } - - if (isRecipeInitialised) { - let metaDataUpdate: any = {}; - - if (firstName.trim() !== "") { - metaDataUpdate["first_name"] = firstName.trim(); - } - - if (lastName.trim() !== "") { - metaDataUpdate["last_name"] = lastName.trim(); - } - - await UserMetadata.updateUserMetadata(userId, metaDataUpdate); - } - } - - if (email.trim() !== "") { - const emailUpdateResponse = await updateEmailForRecipeId(userResponse.recipe, userId, email.trim()); - - if (emailUpdateResponse.status !== "OK") { - return emailUpdateResponse; - } - } - - if (phone.trim() !== "") { - const phoneUpdateResponse = await updatePhoneForRecipeId(userResponse.recipe, userId, phone.trim()); - - if (phoneUpdateResponse.status !== "OK") { - return phoneUpdateResponse; - } - } - - return { - status: "OK", - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts deleted file mode 100644 index a68c04290..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { APIFunction, APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import Session from "../../../session"; - -type SessionType = { - sessionData: any; - accessTokenPayload: any; - userId: string; - expiry: number; - timeCreated: number; - sessionHandle: string; -}; - -type Response = { - status: "OK"; - sessions: SessionType[]; -}; - -export const userSessionsGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { - const userId = options.req.getKeyValueFromQuery("userId"); - - if (userId === undefined) { - throw new STError({ - message: "Missing required parameter 'userId'", - type: STError.BAD_INPUT_ERROR, - }); - } - - const response = await Session.getAllSessionHandlesForUser(userId); - - let sessions: SessionType[] = []; - let sessionInfoPromises: Promise[] = []; - - for (let i = 0; i < response.length; i++) { - sessionInfoPromises.push( - new Promise(async (res, rej) => { - try { - const sessionResponse = await Session.getSessionInformation(response[i]); - - if (sessionResponse !== undefined) { - sessions[i] = sessionResponse; - } - - res(); - } catch (e) { - rej(e); - } - }) - ); - } - - await Promise.all(sessionInfoPromises); - - return { - status: "OK", - sessions, - }; -}; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userSessionsPost.ts b/lib/ts/recipe/dashboard/api/userdetails/userSessionsPost.ts deleted file mode 100644 index 3da9f67bb..000000000 --- a/lib/ts/recipe/dashboard/api/userdetails/userSessionsPost.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { APIInterface, APIOptions } from "../../types"; -import STError from "../../../../error"; -import Session from "../../../session"; - -type Response = { - status: "OK"; -}; - -export const userSessionsPost = async (_: APIInterface, options: APIOptions): Promise => { - const requestBody = await options.req.getJSONBody(); - const sessionHandles = requestBody.sessionHandles; - - if (sessionHandles === undefined || !Array.isArray(sessionHandles)) { - throw new STError({ - message: "Required parameter 'sessionHandles' is missing or has an invalid type", - type: STError.BAD_INPUT_ERROR, - }); - } - - await Session.revokeMultipleSessions(sessionHandles); - return { - status: "OK", - }; -}; diff --git a/lib/ts/recipe/dashboard/api/usersCountGet.ts b/lib/ts/recipe/dashboard/api/usersCountGet.ts deleted file mode 100644 index ba6103047..000000000 --- a/lib/ts/recipe/dashboard/api/usersCountGet.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { APIInterface, APIOptions } from "../types"; -import SuperTokens from "../../../supertokens"; - -export type Response = { - status: "OK"; - count: number; -}; - -export default async function usersCountGet(_: APIInterface, __: APIOptions): Promise { - const count = await SuperTokens.getInstanceOrThrowError().getUserCount(); - - return { - status: "OK", - count, - }; -} diff --git a/lib/ts/recipe/dashboard/api/usersGet.ts b/lib/ts/recipe/dashboard/api/usersGet.ts deleted file mode 100644 index ffe3db2df..000000000 --- a/lib/ts/recipe/dashboard/api/usersGet.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { APIInterface, APIOptions } from "../types"; -import STError from "../../../error"; -import SuperTokens from "../../../supertokens"; -import UserMetaDataRecipe from "../../usermetadata/recipe"; -import UserMetaData from "../../usermetadata"; - -export type Response = { - status: "OK"; - nextPaginationToken?: string; - users: { - recipeId: string; - user: { - id: string; - timeJoined: number; - firstName?: string; - lastName?: string; - } & ( - | { - email: string; - } - | { - email: string; - thirdParty: { - id: string; - userId: string; - }; - } - | { - email?: string; - phoneNumber?: string; - } - ); - }[]; -}; - -export default async function usersGet(_: APIInterface, options: APIOptions): Promise { - const req = options.req; - const limit = options.req.getKeyValueFromQuery("limit"); - - if (limit === undefined) { - throw new STError({ - message: "Missing required parameter 'limit'", - type: STError.BAD_INPUT_ERROR, - }); - } - - let timeJoinedOrder = req.getKeyValueFromQuery("timeJoinedOrder"); - - if (timeJoinedOrder === undefined) { - timeJoinedOrder = "DESC"; - } - - if (timeJoinedOrder !== "ASC" && timeJoinedOrder !== "DESC") { - throw new STError({ - message: "Invalid value recieved for 'timeJoinedOrder'", - type: STError.BAD_INPUT_ERROR, - }); - } - - let paginationToken = options.req.getKeyValueFromQuery("paginationToken"); - const query = getSearchParamsFromURL(options.req.getOriginalURL()); - let usersResponse = await SuperTokens.getInstanceOrThrowError().getUsers({ - query, - timeJoinedOrder: timeJoinedOrder, - limit: parseInt(limit), - paginationToken, - }); - - // If the UserMetaData recipe has been initialised, fetch first and last name - try { - UserMetaDataRecipe.getInstanceOrThrowError(); - } catch (e) { - // Recipe has not been initialised, return without first name and last name - return { - status: "OK", - users: usersResponse.users, - nextPaginationToken: usersResponse.nextPaginationToken, - }; - } - - let updatedUsersArray: { - recipeId: string; - user: any; - }[] = []; - let metaDataFetchPromises: (() => Promise)[] = []; - - for (let i = 0; i < usersResponse.users.length; i++) { - const userObj = usersResponse.users[i]; - metaDataFetchPromises.push( - (): Promise => - new Promise(async (resolve, reject) => { - try { - const userMetaDataResponse = await UserMetaData.getUserMetadata(userObj.user.id); - const { first_name, last_name } = userMetaDataResponse.metadata; - - updatedUsersArray[i] = { - recipeId: userObj.recipeId, - user: { - ...userObj.user, - firstName: first_name, - lastName: last_name, - }, - }; - resolve(true); - } catch (e) { - // Something went wrong when fetching user meta data - reject(e); - } - }) - ); - } - - let promiseArrayStartPosition = 0; - let batchSize = 5; - - while (promiseArrayStartPosition < metaDataFetchPromises.length) { - /** - * We want to query only 5 in parallel at a time - * - * First we check if the the array has enough elements to iterate - * promiseArrayStartPosition + 4 (5 elements including current) - */ - let promiseArrayEndPosition = promiseArrayStartPosition + (batchSize - 1); - - // If the end position is higher than the arrays length, we need to adjust it - if (promiseArrayEndPosition >= metaDataFetchPromises.length) { - /** - * For example if the array has 7 elements [A, B, C, D, E, F, G], when you run - * the second batch [startPosition = 5], this will result in promiseArrayEndPosition - * to be equal to 6 [5 + ((7 - 1) - 5)] and will then iterate over indexes [5] and [6] - */ - promiseArrayEndPosition = - promiseArrayStartPosition + (metaDataFetchPromises.length - 1 - promiseArrayStartPosition); - } - - let promisesToCall: (() => Promise)[] = []; - - for (let j = promiseArrayStartPosition; j <= promiseArrayEndPosition; j++) { - promisesToCall.push(metaDataFetchPromises[j]); - } - - await Promise.all(promisesToCall.map((p) => p())); - promiseArrayStartPosition += batchSize; - } - - usersResponse = { - ...usersResponse, - users: updatedUsersArray, - }; - - return { - status: "OK", - users: usersResponse.users, - nextPaginationToken: usersResponse.nextPaginationToken, - }; -} - -export function getSearchParamsFromURL(path: string): { [key: string]: string } { - const URLObject = new URL("https://exmaple.com" + path); - const params = new URLSearchParams(URLObject.search); - const searchQuery: { [key: string]: string } = {}; - for (const [key, value] of params) { - if (!["limit", "timeJoinedOrder", "paginationToken"].includes(key)) { - searchQuery[key] = value; - } - } - return searchQuery; -} diff --git a/lib/ts/recipe/dashboard/api/validateKey.ts b/lib/ts/recipe/dashboard/api/validateKey.ts deleted file mode 100644 index 0a8d109bc..000000000 --- a/lib/ts/recipe/dashboard/api/validateKey.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { makeDefaultUserContextFromAPI } from "../../../utils"; -import { APIInterface, APIOptions } from "../types"; -import { sendUnauthorisedAccess, validateApiKey } from "../utils"; - -export default async function validateKey(_: APIInterface, options: APIOptions): Promise { - const input = { req: options.req, config: options.config, userContext: makeDefaultUserContextFromAPI(options.req) }; - - if (await validateApiKey(input)) { - options.res.sendJSONResponse({ - status: "OK", - }); - } else { - sendUnauthorisedAccess(options.res); - } - - return true; -} diff --git a/lib/ts/recipe/dashboard/constants.ts b/lib/ts/recipe/dashboard/constants.ts deleted file mode 100644 index 3a148e376..000000000 --- a/lib/ts/recipe/dashboard/constants.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const DASHBOARD_API = "/dashboard"; -export const SIGN_IN_API = "/api/signin"; -export const SIGN_OUT_API = "/api/signout"; -export const VALIDATE_KEY_API = "/api/key/validate"; -export const USERS_LIST_GET_API = "/api/users"; -export const USERS_COUNT_API = "/api/users/count"; -export const USER_API = "/api/user"; -export const USER_EMAIL_VERIFY_API = "/api/user/email/verify"; -export const USER_METADATA_API = "/api/user/metadata"; -export const USER_SESSIONS_API = "/api/user/sessions"; -export const USER_PASSWORD_API = "/api/user/password"; -export const USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token"; -export const SEARCH_TAGS_API = "/api/search/tags"; -export const DASHBOARD_ANALYTICS_API = "/api/analytics"; diff --git a/lib/ts/recipe/dashboard/index.ts b/lib/ts/recipe/dashboard/index.ts deleted file mode 100644 index 33e3b8451..000000000 --- a/lib/ts/recipe/dashboard/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Recipe from "./recipe"; -import { RecipeInterface, APIOptions, APIInterface } from "./types"; - -export default class Wrapper { - static init = Recipe.init; -} - -export let init = Wrapper.init; - -export type { RecipeInterface, APIOptions, APIInterface }; diff --git a/lib/ts/recipe/dashboard/recipe.ts b/lib/ts/recipe/dashboard/recipe.ts deleted file mode 100644 index 6aeec1391..000000000 --- a/lib/ts/recipe/dashboard/recipe.ts +++ /dev/null @@ -1,366 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import OverrideableBuilder from "supertokens-js-override"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { APIFunction, APIInterface, APIOptions, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import RecipeImplementation from "./recipeImplementation"; -import APIImplementation from "./api/implementation"; -import { getApiIdIfMatched, getApiPathWithDashboardBase, isApiPath, validateAndNormaliseUserInput } from "./utils"; -import { - DASHBOARD_ANALYTICS_API, - DASHBOARD_API, - SEARCH_TAGS_API, - SIGN_IN_API, - SIGN_OUT_API, - USERS_COUNT_API, - USERS_LIST_GET_API, - USER_API, - USER_EMAIL_VERIFY_API, - USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, - USER_PASSWORD_API, - USER_SESSIONS_API, - VALIDATE_KEY_API, -} from "./constants"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { BaseRequest, BaseResponse } from "../../framework"; -import dashboard from "./api/dashboard"; -import error from "../../error"; -import validateKey from "./api/validateKey"; -import apiKeyProtector from "./api/apiKeyProtector"; -import usersGet from "./api/usersGet"; -import usersCountGet from "./api/usersCountGet"; -import { userGet } from "./api/userdetails/userGet"; -import { userEmailverifyGet } from "./api/userdetails/userEmailVerifyGet"; -import { userMetaDataGet } from "./api/userdetails/userMetadataGet"; -import { userSessionsGet } from "./api/userdetails/userSessionsGet"; -import { userDelete } from "./api/userdetails/userDelete"; -import { userEmailVerifyPut } from "./api/userdetails/userEmailVerifyPut"; -import { userMetadataPut } from "./api/userdetails/userMetadataPut"; -import { userPasswordPut } from "./api/userdetails/userPasswordPut"; -import { userPut } from "./api/userdetails/userPut"; -import { userEmailVerifyTokenPost } from "./api/userdetails/userEmailVerifyTokenPost"; -import { userSessionsPost } from "./api/userdetails/userSessionsPost"; -import signIn from "./api/signIn"; -import signOut from "./api/signOut"; -import { getSearchTags } from "./api/search/tagsGet"; -import analyticsPost from "./api/analytics"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "dashboard"; - - config: TypeNormalisedInput; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { - super(recipeId, appInfo); - - this.config = validateAndNormaliseUserInput(config); - this.isInServerlessEnv = isInServerlessEnv; - - { - let builder = new OverrideableBuilder(RecipeImplementation()); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("Dashboard recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - // abstract instance functions below............... - - getAPIsHandled = (): APIHandled[] => { - /** - * Normally this array is used by the SDK to decide whether or not the recipe - * handles a specific API path and method and then returns the ID. - * - * For the dashboard recipe this logic is fully custom and handled inside the - * `returnAPIIdIfCanHandleRequest` method of this class. - * - * For most frameworks this array is redundant because the `returnAPIIdIfCanHandleRequest` is used. - * But for frameworks such as Hapi that require all APIs to be declared up front, this array is used - * to make sure that the framework does not return a 404 - */ - return [ - { - id: DASHBOARD_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(DASHBOARD_API)), - disabled: false, - method: "get", - }, - { - id: SIGN_IN_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(SIGN_IN_API)), - disabled: false, - method: "post", - }, - { - id: VALIDATE_KEY_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(VALIDATE_KEY_API)), - disabled: false, - method: "post", - }, - { - id: SIGN_OUT_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(SIGN_OUT_API)), - disabled: false, - method: "post", - }, - { - id: USERS_LIST_GET_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USERS_LIST_GET_API)), - disabled: false, - method: "get", - }, - { - id: USERS_COUNT_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USERS_COUNT_API)), - disabled: false, - method: "get", - }, - { - id: USER_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_API)), - disabled: false, - method: "get", - }, - { - id: USER_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_API)), - disabled: false, - method: "post", - }, - { - id: USER_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_API)), - disabled: false, - method: "delete", - }, - { - id: USER_EMAIL_VERIFY_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_EMAIL_VERIFY_API)), - disabled: false, - method: "get", - }, - { - id: USER_EMAIL_VERIFY_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_EMAIL_VERIFY_API)), - disabled: false, - method: "put", - }, - { - id: USER_METADATA_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_METADATA_API)), - disabled: false, - method: "get", - }, - { - id: USER_METADATA_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_METADATA_API)), - disabled: false, - method: "put", - }, - { - id: USER_SESSIONS_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_SESSIONS_API)), - disabled: false, - method: "get", - }, - { - id: USER_SESSIONS_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_SESSIONS_API)), - disabled: false, - method: "post", - }, - { - id: USER_PASSWORD_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_PASSWORD_API)), - disabled: false, - method: "put", - }, - { - id: USER_EMAIL_VERIFY_TOKEN_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_EMAIL_VERIFY_TOKEN_API)), - disabled: false, - method: "post", - }, - { - id: SEARCH_TAGS_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(SEARCH_TAGS_API)), - disabled: false, - method: "get", - }, - { - id: DASHBOARD_ANALYTICS_API, - pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(DASHBOARD_ANALYTICS_API)), - disabled: false, - method: "post", - }, - ]; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - __: NormalisedURLPath, - ___: HTTPMethod - ): Promise => { - let options: APIOptions = { - config: this.config, - recipeId: this.getRecipeId(), - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - isInServerlessEnv: this.isInServerlessEnv, - appInfo: this.getAppInfo(), - }; - - // For these APIs we dont need API key validation - if (id === DASHBOARD_API) { - return await dashboard(this.apiImpl, options); - } - - if (id === SIGN_IN_API) { - return await signIn(this.apiImpl, options); - } - - if (id === VALIDATE_KEY_API) { - return await validateKey(this.apiImpl, options); - } - - // Do API key validation for the remaining APIs - let apiFunction: APIFunction | undefined; - - if (id === USERS_LIST_GET_API) { - apiFunction = usersGet; - } else if (id === USERS_COUNT_API) { - apiFunction = usersCountGet; - } else if (id === USER_API) { - if (req.getMethod() === "get") { - apiFunction = userGet; - } - - if (req.getMethod() === "delete") { - apiFunction = userDelete; - } - - if (req.getMethod() === "put") { - apiFunction = userPut; - } - } else if (id === USER_EMAIL_VERIFY_API) { - if (req.getMethod() === "get") { - apiFunction = userEmailverifyGet; - } - - if (req.getMethod() === "put") { - apiFunction = userEmailVerifyPut; - } - } else if (id === USER_METADATA_API) { - if (req.getMethod() === "get") { - apiFunction = userMetaDataGet; - } - - if (req.getMethod() === "put") { - apiFunction = userMetadataPut; - } - } else if (id === USER_SESSIONS_API) { - if (req.getMethod() === "get") { - apiFunction = userSessionsGet; - } - - if (req.getMethod() === "post") { - apiFunction = userSessionsPost; - } - } else if (id === USER_PASSWORD_API) { - apiFunction = userPasswordPut; - } else if (id === USER_EMAIL_VERIFY_TOKEN_API) { - apiFunction = userEmailVerifyTokenPost; - } else if (id === SEARCH_TAGS_API) { - apiFunction = getSearchTags; - } else if (id === SIGN_OUT_API) { - apiFunction = signOut; - } else if (id === DASHBOARD_ANALYTICS_API && req.getMethod() === "post") { - apiFunction = analyticsPost; - } - - // If the id doesnt match any APIs return false - if (apiFunction === undefined) { - return false; - } - - return await apiKeyProtector(this.apiImpl, options, apiFunction); - }; - - handleError = async (err: error, _: BaseRequest, __: BaseResponse): Promise => { - throw err; - }; - - getAllCORSHeaders = (): string[] => { - return []; - }; - - isErrorFromThisRecipe = (err: any): err is error => { - return error.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - - returnAPIIdIfCanHandleRequest = (path: NormalisedURLPath, method: HTTPMethod): string | undefined => { - const dashboardBundlePath = this.getAppInfo().apiBasePath.appendPath(new NormalisedURLPath(DASHBOARD_API)); - - if (isApiPath(path, this.getAppInfo())) { - return getApiIdIfMatched(path, method); - } - - if (path.startsWith(dashboardBundlePath)) { - return DASHBOARD_API; - } - - return undefined; - }; -} diff --git a/lib/ts/recipe/dashboard/recipeImplementation.ts b/lib/ts/recipe/dashboard/recipeImplementation.ts deleted file mode 100644 index d40b8297f..000000000 --- a/lib/ts/recipe/dashboard/recipeImplementation.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import NormalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; -import { dashboardVersion } from "../../version"; -import { RecipeInterface } from "./types"; -import { validateApiKey } from "./utils"; - -export default function getRecipeImplementation(): RecipeInterface { - return { - getDashboardBundleLocation: async function () { - return `https://cdn.jsdelivr.net/gh/supertokens/dashboard@v${dashboardVersion}/build/`; - }, - shouldAllowAccess: async function (input) { - // For cases where we're not using the API key, the JWT is being used; we allow their access by default - if (!input.config.apiKey) { - // make the check for the API endpoint here with querier - let querier = Querier.getNewInstanceOrThrowError(undefined); - const authHeaderValue = input.req.getHeaderValue("authorization")?.split(" ")[1]; - const sessionVerificationResponse = await querier.sendPostRequest( - new NormalisedURLPath("/recipe/dashboard/session/verify"), - { - sessionId: authHeaderValue, - } - ); - return sessionVerificationResponse.status === "OK"; - } - return await validateApiKey(input); - }, - }; -} diff --git a/lib/ts/recipe/dashboard/types.ts b/lib/ts/recipe/dashboard/types.ts deleted file mode 100644 index dd1995b1f..000000000 --- a/lib/ts/recipe/dashboard/types.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; - -export type TypeInput = { - apiKey?: string; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - apiKey?: string; - authMode: AuthMode; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type RecipeInterface = { - getDashboardBundleLocation(input: { userContext: any }): Promise; - shouldAllowAccess(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }): Promise; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - req: BaseRequest; - res: BaseResponse; - isInServerlessEnv: boolean; - appInfo: NormalisedAppinfo; -}; - -export type APIInterface = { - dashboardGET: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); -}; - -export type APIFunction = (apiImplementation: APIInterface, options: APIOptions) => Promise; - -export type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; - -export type AuthMode = "api-key" | "email-password"; - -type CommonUserInformation = { - id: string; - timeJoined: number; - firstName: string; - lastName: string; -}; - -export type EmailPasswordUser = CommonUserInformation & { - email: string; -}; - -export type ThirdPartyUser = CommonUserInformation & { - email: string; - thirdParty: { - id: string; - userId: string; - }; -}; - -export type PasswordlessUser = CommonUserInformation & { - email?: string; - phone?: string; -}; diff --git a/lib/ts/recipe/dashboard/utils.ts b/lib/ts/recipe/dashboard/utils.ts deleted file mode 100644 index 9774740b2..000000000 --- a/lib/ts/recipe/dashboard/utils.ts +++ /dev/null @@ -1,375 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { HTTPMethod, NormalisedAppinfo } from "../../types"; -import { sendNon200ResponseWithMessage } from "../../utils"; -import { - DASHBOARD_API, - SEARCH_TAGS_API, - SIGN_IN_API, - SIGN_OUT_API, - USERS_COUNT_API, - USERS_LIST_GET_API, - USER_API, - USER_EMAIL_VERIFY_API, - USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, - USER_PASSWORD_API, - USER_SESSIONS_API, - VALIDATE_KEY_API, - DASHBOARD_ANALYTICS_API, -} from "./constants"; -import { - APIInterface, - EmailPasswordUser, - PasswordlessUser, - RecipeIdForUser, - RecipeInterface, - ThirdPartyUser, - TypeInput, - TypeNormalisedInput, -} from "./types"; -import EmailPasswordRecipe from "../emailpassword/recipe"; -import ThirdPartyRecipe from "../thirdparty/recipe"; -import PasswordlessRecipe from "../passwordless/recipe"; -import EmailPassword from "../emailpassword"; -import ThirdParty from "../thirdparty"; -import Passwordless from "../passwordless"; -import ThirdPartyEmailPassword from "../thirdpartyemailpassword"; -import ThirdPartyEmailPasswordRecipe from "../thirdpartyemailpassword/recipe"; -import ThirdPartyPasswordless from "../thirdpartypasswordless"; -import ThirdPartyPasswordlessRecipe from "../thirdpartypasswordless/recipe"; - -export function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput { - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...(config === undefined ? {} : config.override), - }; - - return { - override, - authMode: config !== undefined && config.apiKey ? "api-key" : "email-password", - }; -} - -export function isApiPath(path: NormalisedURLPath, appInfo: NormalisedAppinfo): boolean { - const dashboardRecipeBasePath = appInfo.apiBasePath.appendPath(new NormalisedURLPath(DASHBOARD_API)); - if (!path.startsWith(dashboardRecipeBasePath)) { - return false; - } - - let pathWithoutDashboardPath = path.getAsStringDangerous().split(DASHBOARD_API)[1]; - - if (pathWithoutDashboardPath.charAt(0) === "/") { - pathWithoutDashboardPath = pathWithoutDashboardPath.substring(1, pathWithoutDashboardPath.length); - } - - if (pathWithoutDashboardPath.split("/")[0] === "api") { - return true; - } - - return false; -} - -export function getApiIdIfMatched(path: NormalisedURLPath, method: HTTPMethod): string | undefined { - if (path.getAsStringDangerous().endsWith(VALIDATE_KEY_API) && method === "post") { - return VALIDATE_KEY_API; - } - - if (path.getAsStringDangerous().endsWith(SIGN_IN_API) && method === "post") { - return SIGN_IN_API; - } - - if (path.getAsStringDangerous().endsWith(SIGN_OUT_API) && method === "post") { - return SIGN_OUT_API; - } - - if (path.getAsStringDangerous().endsWith(USERS_LIST_GET_API) && method === "get") { - return USERS_LIST_GET_API; - } - - if (path.getAsStringDangerous().endsWith(USERS_COUNT_API) && method === "get") { - return USERS_COUNT_API; - } - - if (path.getAsStringDangerous().endsWith(USER_API)) { - if (method === "get" || method === "delete" || method === "put") { - return USER_API; - } - } - - if (path.getAsStringDangerous().endsWith(USER_EMAIL_VERIFY_API)) { - if (method === "get" || method === "put") { - return USER_EMAIL_VERIFY_API; - } - } - - if (path.getAsStringDangerous().endsWith(USER_METADATA_API)) { - if (method === "get" || method === "put") { - return USER_METADATA_API; - } - } - - if (path.getAsStringDangerous().endsWith(USER_SESSIONS_API)) { - if (method === "get" || method === "post") { - return USER_SESSIONS_API; - } - } - - if (path.getAsStringDangerous().endsWith(USER_PASSWORD_API) && method === "put") { - return USER_PASSWORD_API; - } - - if (path.getAsStringDangerous().endsWith(USER_EMAIL_VERIFY_TOKEN_API) && method === "post") { - return USER_EMAIL_VERIFY_TOKEN_API; - } - - if (path.getAsStringDangerous().endsWith(USER_PASSWORD_API) && method === "put") { - return USER_PASSWORD_API; - } - if (path.getAsStringDangerous().endsWith(SEARCH_TAGS_API) && method === "get") { - return SEARCH_TAGS_API; - } - - if (path.getAsStringDangerous().endsWith(DASHBOARD_ANALYTICS_API) && method === "post") { - return DASHBOARD_ANALYTICS_API; - } - - return undefined; -} - -export function sendUnauthorisedAccess(res: BaseResponse) { - sendNon200ResponseWithMessage(res, "Unauthorised access", 401); -} - -export function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser { - return recipeId === "emailpassword" || recipeId === "thirdparty" || recipeId === "passwordless"; -} - -export async function getUserForRecipeId( - userId: string, - recipeId: string -): Promise<{ - user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined; - recipe: - | "emailpassword" - | "thirdparty" - | "passwordless" - | "thirdpartyemailpassword" - | "thirdpartypasswordless" - | undefined; -}> { - let user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined; - let recipe: - | "emailpassword" - | "thirdparty" - | "passwordless" - | "thirdpartyemailpassword" - | "thirdpartypasswordless" - | undefined; - - if (recipeId === EmailPasswordRecipe.RECIPE_ID) { - try { - const userResponse = await EmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "emailpassword"; - } - } catch (e) { - // No - op - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyEmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === ThirdPartyRecipe.RECIPE_ID) { - try { - const userResponse = await ThirdParty.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdparty"; - } - } catch (e) { - // No - op - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyEmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyPasswordless.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === PasswordlessRecipe.RECIPE_ID) { - try { - const userResponse = await Passwordless.getUserById({ - userId, - }); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "passwordless"; - } - } catch (e) { - // No - op - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyPasswordless.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - firstName: "", - lastName: "", - }; - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } - - return { - user, - recipe, - }; -} - -export function isRecipeInitialised(recipeId: RecipeIdForUser): boolean { - let isRecipeInitialised = false; - - if (recipeId === "emailpassword") { - try { - EmailPasswordRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - - if (!isRecipeInitialised) { - try { - ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - } else if (recipeId === "passwordless") { - try { - PasswordlessRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - - if (!isRecipeInitialised) { - try { - ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - } else if (recipeId === "thirdparty") { - try { - ThirdPartyRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - - if (!isRecipeInitialised) { - try { - ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - - if (!isRecipeInitialised) { - try { - ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); - isRecipeInitialised = true; - } catch (_) {} - } - } - - return isRecipeInitialised; -} - -export async function validateApiKey(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }) { - let apiKeyHeaderValue: string | undefined = input.req.getHeaderValue("authorization"); - - // We receieve the api key as `Bearer API_KEY`, this retrieves just the key - apiKeyHeaderValue = apiKeyHeaderValue?.split(" ")[1]; - - if (apiKeyHeaderValue === undefined) { - return false; - } - - return apiKeyHeaderValue === input.config.apiKey; -} - -export function getApiPathWithDashboardBase(path: string): string { - return DASHBOARD_API + path; -} diff --git a/lib/ts/recipe/emailpassword/api/emailExists.ts b/lib/ts/recipe/emailpassword/api/emailExists.ts deleted file mode 100644 index 96bca2e95..000000000 --- a/lib/ts/recipe/emailpassword/api/emailExists.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function emailExists(apiImplementation: APIInterface, options: APIOptions): Promise { - // Logic as per https://github.com/supertokens/supertokens-node/issues/47#issue-751571692 - - if (apiImplementation.emailExistsGET === undefined) { - return false; - } - - let email = options.req.getKeyValueFromQuery("email"); - - if (email === undefined || typeof email !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the email as a GET param", - }); - } - - let result = await apiImplementation.emailExistsGET({ - email, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts b/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts deleted file mode 100644 index 60f0cce2a..000000000 --- a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { validateFormFieldsOrThrowError } from "./utils"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function generatePasswordResetToken( - apiImplementation: APIInterface, - options: APIOptions -): Promise { - // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 - - if (apiImplementation.generatePasswordResetTokenPOST === undefined) { - return false; - } - - // step 1 - let formFields: { - id: string; - value: string; - }[] = await validateFormFieldsOrThrowError( - options.config.resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm, - (await options.req.getJSONBody()).formFields - ); - - let result = await apiImplementation.generatePasswordResetTokenPOST({ - formFields, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts deleted file mode 100644 index 2852e69c2..000000000 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { APIInterface, APIOptions, User } from "../"; -import { logDebugMessage } from "../../../logger"; -import Session from "../../session"; -import { SessionContainerInterface } from "../../session/types"; -import { GeneralErrorResponse } from "../../../types"; - -export default function getAPIImplementation(): APIInterface { - return { - emailExistsGET: async function ({ - email, - options, - userContext, - }: { - email: string; - options: APIOptions; - userContext: any; - }): Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - > { - let user = await options.recipeImplementation.getUserByEmail({ email, userContext }); - - return { - status: "OK", - exists: user !== undefined, - }; - }, - generatePasswordResetTokenPOST: async function ({ - formFields, - options, - userContext, - }: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }): Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - > { - let email = formFields.filter((f) => f.id === "email")[0].value; - - let user = await options.recipeImplementation.getUserByEmail({ email, userContext }); - if (user === undefined) { - return { - status: "OK", - }; - } - - let response = await options.recipeImplementation.createResetPasswordToken({ - userId: user.id, - userContext, - }); - if (response.status === "UNKNOWN_USER_ID_ERROR") { - logDebugMessage(`Password reset email not sent, unknown user id: ${user.id}`); - return { - status: "OK", - }; - } - - let passwordResetLink = - options.appInfo.websiteDomain.getAsStringDangerous() + - options.appInfo.websiteBasePath.getAsStringDangerous() + - "/reset-password?token=" + - response.token + - "&rid=" + - options.recipeId; - - logDebugMessage(`Sending password reset email to ${email}`); - await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORD_RESET", - user, - passwordResetLink, - userContext, - }); - - return { - status: "OK", - }; - }, - passwordResetPOST: async function ({ - formFields, - token, - options, - userContext, - }: { - formFields: { - id: string; - value: string; - }[]; - token: string; - options: APIOptions; - userContext: any; - }): Promise< - | { - status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; - } - | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } - | GeneralErrorResponse - > { - let newPassword = formFields.filter((f) => f.id === "password")[0].value; - - let response = await options.recipeImplementation.resetPasswordUsingToken({ - token, - newPassword, - userContext, - }); - - return response; - }, - signInPOST: async function ({ - formFields, - options, - userContext, - }: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }): Promise< - | { - status: "OK"; - session: SessionContainerInterface; - user: User; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - | GeneralErrorResponse - > { - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; - - let response = await options.recipeImplementation.signIn({ email, password, userContext }); - if (response.status === "WRONG_CREDENTIALS_ERROR") { - return response; - } - let user = response.user; - - let session = await Session.createNewSession(options.req, options.res, user.id, {}, {}, userContext); - return { - status: "OK", - session, - user, - }; - }, - signUpPOST: async function ({ - formFields, - options, - userContext, - }: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }): Promise< - | { - status: "OK"; - session: SessionContainerInterface; - user: User; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | GeneralErrorResponse - > { - let email = formFields.filter((f) => f.id === "email")[0].value; - let password = formFields.filter((f) => f.id === "password")[0].value; - - let response = await options.recipeImplementation.signUp({ email, password, userContext }); - if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return response; - } - let user = response.user; - - let session = await Session.createNewSession(options.req, options.res, user.id, {}, {}, userContext); - return { - status: "OK", - session, - user, - }; - }, - }; -} diff --git a/lib/ts/recipe/emailpassword/api/passwordReset.ts b/lib/ts/recipe/emailpassword/api/passwordReset.ts deleted file mode 100644 index f0ad8ca5b..000000000 --- a/lib/ts/recipe/emailpassword/api/passwordReset.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { validateFormFieldsOrThrowError } from "./utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function passwordReset(apiImplementation: APIInterface, options: APIOptions): Promise { - // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 - - if (apiImplementation.passwordResetPOST === undefined) { - return false; - } - - // step 1 - let formFields: { - id: string; - value: string; - }[] = await validateFormFieldsOrThrowError( - options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, - (await options.req.getJSONBody()).formFields - ); - - let token = (await options.req.getJSONBody()).token; - if (token === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the password reset token", - }); - } - if (typeof token !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "The password reset token must be a string", - }); - } - - let result = await apiImplementation.passwordResetPOST({ - formFields, - token, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - send200Response( - options.res, - result.status === "OK" - ? { - status: "OK", - } - : result - ); - return true; -} diff --git a/lib/ts/recipe/emailpassword/api/signin.ts b/lib/ts/recipe/emailpassword/api/signin.ts deleted file mode 100644 index b31517c8c..000000000 --- a/lib/ts/recipe/emailpassword/api/signin.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { validateFormFieldsOrThrowError } from "./utils"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function signInAPI(apiImplementation: APIInterface, options: APIOptions): Promise { - // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 - if (apiImplementation.signInPOST === undefined) { - return false; - } - - // step 1 - let formFields: { - id: string; - value: string; - }[] = await validateFormFieldsOrThrowError( - options.config.signInFeature.formFields, - (await options.req.getJSONBody()).formFields - ); - - let result = await apiImplementation.signInPOST({ - formFields, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - if (result.status === "OK") { - send200Response(options.res, { - status: "OK", - user: result.user, - }); - } else { - send200Response(options.res, result); - } - return true; -} diff --git a/lib/ts/recipe/emailpassword/api/signup.ts b/lib/ts/recipe/emailpassword/api/signup.ts deleted file mode 100644 index ff768fdd5..000000000 --- a/lib/ts/recipe/emailpassword/api/signup.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { validateFormFieldsOrThrowError } from "./utils"; -import { APIInterface, APIOptions } from "../"; -import STError from "../error"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function signUpAPI(apiImplementation: APIInterface, options: APIOptions): Promise { - // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 - - if (apiImplementation.signUpPOST === undefined) { - return false; - } - - // step 1 - let formFields: { - id: string; - value: string; - }[] = await validateFormFieldsOrThrowError( - options.config.signUpFeature.formFields, - (await options.req.getJSONBody()).formFields - ); - - let result = await apiImplementation.signUpPOST({ - formFields, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - send200Response(options.res, { - status: "OK", - user: result.user, - }); - } else if (result.status === "GENERAL_ERROR") { - send200Response(options.res, result); - } else { - throw new STError({ - type: STError.FIELD_ERROR, - payload: [ - { - id: "email", - error: "This email already exists. Please sign in instead.", - }, - ], - message: "Error in input formFields", - }); - } - return true; -} diff --git a/lib/ts/recipe/emailpassword/api/utils.ts b/lib/ts/recipe/emailpassword/api/utils.ts deleted file mode 100644 index aca7cbab8..000000000 --- a/lib/ts/recipe/emailpassword/api/utils.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { NormalisedFormField } from "../types"; -import STError from "../error"; -import { FORM_FIELD_EMAIL_ID } from "../constants"; - -export async function validateFormFieldsOrThrowError( - configFormFields: NormalisedFormField[], - formFieldsRaw: any -): Promise< - { - id: string; - value: string; - }[] -> { - // first we check syntax ---------------------------- - if (formFieldsRaw === undefined) { - throw newBadRequestError("Missing input param: formFields"); - } - - if (!Array.isArray(formFieldsRaw)) { - throw newBadRequestError("formFields must be an array"); - } - - let formFields: { - id: string; - value: string; - }[] = []; - - for (let i = 0; i < formFieldsRaw.length; i++) { - let curr = formFieldsRaw[i]; - if (typeof curr !== "object" || curr === null) { - throw newBadRequestError("All elements of formFields must be an object"); - } - if (typeof curr.id !== "string" || curr.value === undefined) { - throw newBadRequestError("All elements of formFields must contain an 'id' and 'value' field"); - } - formFields.push(curr); - } - - // we trim the email: https://github.com/supertokens/supertokens-core/issues/99 - formFields = formFields.map((field) => { - if (field.id === FORM_FIELD_EMAIL_ID) { - return { - ...field, - value: field.value.trim(), - }; - } - return field; - }); - - // then run validators through them----------------------- - await validateFormOrThrowError(formFields, configFormFields); - - return formFields; -} - -function newBadRequestError(message: string) { - return new STError({ - type: STError.BAD_INPUT_ERROR, - message, - }); -} - -// We check that the number of fields in input and config form field is the same. -// We check that each item in the config form field is also present in the input form field -async function validateFormOrThrowError( - inputs: { - id: string; - value: string; - }[], - configFormFields: NormalisedFormField[] -) { - let validationErrors: { id: string; error: string }[] = []; - - if (configFormFields.length !== inputs.length) { - throw newBadRequestError("Are you sending too many / too few formFields?"); - } - - // Loop through all form fields. - for (let i = 0; i < configFormFields.length; i++) { - const field = configFormFields[i]; - - // Find corresponding input value. - const input = inputs.find((i) => i.id === field.id); - - // Absent or not optional empty field - if (input === undefined || (input.value === "" && !field.optional)) { - validationErrors.push({ - error: "Field is not optional", - id: field.id, - }); - } else { - // Otherwise, use validate function. - const error = await field.validate(input.value); - // If error, add it. - if (error !== undefined) { - validationErrors.push({ - error, - id: field.id, - }); - } - } - } - - if (validationErrors.length !== 0) { - throw new STError({ - type: STError.FIELD_ERROR, - payload: validationErrors, - message: "Error in input formFields", - }); - } -} diff --git a/lib/ts/recipe/emailpassword/constants.ts b/lib/ts/recipe/emailpassword/constants.ts deleted file mode 100644 index 730d815c7..000000000 --- a/lib/ts/recipe/emailpassword/constants.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const FORM_FIELD_PASSWORD_ID = "password"; - -export const FORM_FIELD_EMAIL_ID = "email"; - -export const SIGN_UP_API = "/signup"; - -export const SIGN_IN_API = "/signin"; - -export const GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; - -export const PASSWORD_RESET_API = "/user/password/reset"; - -export const SIGNUP_EMAIL_EXISTS_API = "/signup/email/exists"; diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts deleted file mode 100644 index 6bdeaa072..000000000 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeEmailPasswordEmailDeliveryInput, User, RecipeInterface } from "../../../types"; -import { createAndSendCustomEmail as defaultCreateAndSendCustomEmail } from "../../../passwordResetFunctions"; -import { NormalisedAppinfo } from "../../../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; - -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private recipeInterfaceImpl: RecipeInterface; - private isInServerlessEnv: boolean; - private appInfo: NormalisedAppinfo; - private resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise; - }; - - constructor( - recipeInterfaceImpl: RecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } - ) { - this.recipeInterfaceImpl = recipeInterfaceImpl; - this.isInServerlessEnv = isInServerlessEnv; - this.appInfo = appInfo; - { - let inputCreateAndSendCustomEmail = resetPasswordUsingTokenFeature?.createAndSendCustomEmail; - this.resetPasswordUsingTokenFeature = - inputCreateAndSendCustomEmail !== undefined - ? { - createAndSendCustomEmail: inputCreateAndSendCustomEmail, - } - : { - createAndSendCustomEmail: defaultCreateAndSendCustomEmail(this.appInfo), - }; - } - } - - sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }) => { - let user = await this.recipeInterfaceImpl.getUserById({ - userId: input.user.id, - userContext: input.userContext, - }); - if (user === undefined) { - throw Error("this should never come here"); - } - // we add this here cause the user may have overridden the sendEmail function - // to change the input email and if we don't do this, the input email - // will get reset by the getUserById call above. - user.email = input.user.email; - try { - if (!this.isInServerlessEnv) { - this.resetPasswordUsingTokenFeature - .createAndSendCustomEmail(user, input.passwordResetLink, input.userContext) - .catch((_) => {}); - } else { - // see https://github.com/supertokens/supertokens-node/pull/135 - await this.resetPasswordUsingTokenFeature.createAndSendCustomEmail( - user, - input.passwordResetLink, - input.userContext - ); - } - } catch (_) {} - }; -} diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/index.ts deleted file mode 100644 index d70e3f387..000000000 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import SMTP from "./smtp"; -export let SMTPService = SMTP; diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/index.ts deleted file mode 100644 index 974dfdb07..000000000 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { TypeEmailPasswordEmailDeliveryInput } from "../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { createTransport } from "nodemailer"; -import OverrideableBuilder from "supertokens-js-override"; -import { getServiceImplementation } from "./serviceImplementation"; - -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - - constructor(config: TypeInput) { - const transporter = createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } - - sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }) => { - let content = await this.serviceImpl.getContent(input); - await this.serviceImpl.sendRawEmail({ - ...content, - userContext: input.userContext, - }); - }; -} diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.ts deleted file mode 100644 index 2c69d1993..000000000 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.ts +++ /dev/null @@ -1,940 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeEmailPasswordPasswordResetEmailDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/emaildelivery/services/smtp"; -import Supertokens from "../../../../../supertokens"; -export default function getPasswordResetEmailContent( - input: TypeEmailPasswordPasswordResetEmailDeliveryInput -): GetContentResult { - let supertokens = Supertokens.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getPasswordResetEmailHTML(appName, input.user.email, input.passwordResetLink); - return { - body, - toEmail: input.user.email, - subject: "Password reset instructions", - isHtml: true, - }; -} - -export function getPasswordResetEmailHTML(appName: string, email: string, resetLink: string) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
- - -
-
- -

- A password reset request for your account on - ${appName} has been received. -

- - -
-
-

- Alternatively, you can directly paste this link - in your browser
- ${resetLink} -

-
-
- - - - -
- - - - - - -
-

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts deleted file mode 100644 index b16738eea..000000000 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypeEmailPasswordEmailDeliveryInput } from "../../../../types"; -import { Transporter } from "nodemailer"; -import { - ServiceInterface, - TypeInputSendRawEmail, - GetContentResult, -} from "../../../../../../ingredients/emaildelivery/services/smtp"; -import getPasswordResetEmailContent from "../passwordReset"; - -export function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface { - return { - sendRawEmail: async function (input: TypeInputSendRawEmail) { - if (input.isHtml) { - await transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - await transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }, - getContent: async function ( - input: TypeEmailPasswordEmailDeliveryInput & { userContext: any } - ): Promise { - return getPasswordResetEmailContent(input); - }, - }; -} diff --git a/lib/ts/recipe/emailpassword/error.ts b/lib/ts/recipe/emailpassword/error.ts deleted file mode 100644 index fe4928098..000000000 --- a/lib/ts/recipe/emailpassword/error.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; - -export default class SessionError extends STError { - static FIELD_ERROR: "FIELD_ERROR" = "FIELD_ERROR"; - - constructor( - options: - | { - type: "FIELD_ERROR"; - payload: { - id: string; - error: string; - }[]; - message: string; - } - | { - type: "BAD_INPUT_ERROR"; - message: string; - } - ) { - super({ - ...options, - }); - this.fromRecipe = "emailpassword"; - } -} diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts deleted file mode 100644 index bd631ee66..000000000 --- a/lib/ts/recipe/emailpassword/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { RecipeInterface, User, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; - -export default class Wrapper { - static init = Recipe.init; - - static Error = SuperTokensError; - - static signUp(email: string, password: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signUp({ - email, - password, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static signIn(email: string, password: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signIn({ - email, - password, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static getUserById(userId: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static getUserByEmail(email: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ - email, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static createResetPasswordToken(userId: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static resetPasswordUsingToken(token: string, newPassword: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ - token, - newPassword, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static updateEmailOrPassword(input: { userId: string; email?: string; password?: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ - userContext: {}, - ...input, - }); - } - - static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any }) { - let recipeInstance = Recipe.getInstanceOrThrowError(); - return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, - ...input, - }); - } -} - -export let init = Wrapper.init; - -export let Error = Wrapper.Error; - -export let signUp = Wrapper.signUp; - -export let signIn = Wrapper.signIn; - -export let getUserById = Wrapper.getUserById; - -export let getUserByEmail = Wrapper.getUserByEmail; - -export let createResetPasswordToken = Wrapper.createResetPasswordToken; - -export let resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; - -export let updateEmailOrPassword = Wrapper.updateEmailOrPassword; - -export type { RecipeInterface, User, APIOptions, APIInterface }; - -export let sendEmail = Wrapper.sendEmail; diff --git a/lib/ts/recipe/emailpassword/passwordResetFunctions.ts b/lib/ts/recipe/emailpassword/passwordResetFunctions.ts deleted file mode 100644 index d5671a545..000000000 --- a/lib/ts/recipe/emailpassword/passwordResetFunctions.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { User } from "./types"; -import { NormalisedAppinfo } from "../../types"; -import axios, { AxiosError } from "axios"; -import { logDebugMessage } from "../../logger"; - -export function createAndSendCustomEmail(appInfo: NormalisedAppinfo) { - return async (user: User, passwordResetURLWithToken: string) => { - // related issue: https://github.com/supertokens/supertokens-node/issues/38 - if (process.env.TEST_MODE === "testing") { - return; - } - try { - await axios({ - method: "POST", - url: "https://api.supertokens.io/0/st/auth/password/reset", - data: { - email: user.email, - appName: appInfo.appName, - passwordResetURL: passwordResetURLWithToken, - }, - headers: { - "api-version": 0, - }, - }); - logDebugMessage(`Password reset email sent to ${user.email}`); - } catch (error) { - logDebugMessage("Error sending password reset email"); - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - if (err.response) { - logDebugMessage(`Error status: ${err.response.status}`); - logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logDebugMessage(`Error: ${err.message}`); - } - } else { - logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logDebugMessage("Logging the input below:"); - logDebugMessage( - JSON.stringify( - { - email: user.email, - appName: appInfo.appName, - passwordResetURL: passwordResetURLWithToken, - }, - null, - 2 - ) - ); - } - }; -} diff --git a/lib/ts/recipe/emailpassword/recipe.ts b/lib/ts/recipe/emailpassword/recipe.ts deleted file mode 100644 index 094ad0e4c..000000000 --- a/lib/ts/recipe/emailpassword/recipe.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction } from "../../types"; -import STError from "./error"; -import { validateAndNormaliseUserInput } from "./utils"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { - SIGN_UP_API, - SIGN_IN_API, - GENERATE_PASSWORD_RESET_TOKEN_API, - PASSWORD_RESET_API, - SIGNUP_EMAIL_EXISTS_API, -} from "./constants"; -import signUpAPI from "./api/signup"; -import signInAPI from "./api/signin"; -import generatePasswordResetTokenAPI from "./api/generatePasswordResetToken"; -import passwordResetAPI from "./api/passwordReset"; -import { send200Response } from "../../utils"; -import emailExistsAPI from "./api/emailExists"; -import EmailVerificationRecipe from "../emailverification/recipe"; -import RecipeImplementation from "./recipeImplementation"; -import APIImplementation from "./api/implementation"; -import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { TypeEmailPasswordEmailDeliveryInput } from "./types"; -import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "emailpassword"; - - config: TypeNormalisedInput; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - emailDelivery: EmailDeliveryIngredient; - - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput | undefined, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - } - ) { - super(recipeId, appInfo); - this.isInServerlessEnv = isInServerlessEnv; - this.config = validateAndNormaliseUserInput(this, appInfo, config); - { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new EmailDeliveryIngredient( - this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv) - ) - : ingredients.emailDelivery; - - PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = EmailVerificationRecipe.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { - emailDelivery: undefined, - }); - return Recipe.instance; - } else { - throw new Error("Emailpassword recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - // abstract instance functions below............... - - getAPIsHandled = (): APIHandled[] => { - return [ - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(SIGN_UP_API), - id: SIGN_UP_API, - disabled: this.apiImpl.signUpPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(SIGN_IN_API), - id: SIGN_IN_API, - disabled: this.apiImpl.signInPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(GENERATE_PASSWORD_RESET_TOKEN_API), - id: GENERATE_PASSWORD_RESET_TOKEN_API, - disabled: this.apiImpl.generatePasswordResetTokenPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(PASSWORD_RESET_API), - id: PASSWORD_RESET_API, - disabled: this.apiImpl.passwordResetPOST === undefined, - }, - { - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(SIGNUP_EMAIL_EXISTS_API), - id: SIGNUP_EMAIL_EXISTS_API, - disabled: this.apiImpl.emailExistsGET === undefined, - }, - ]; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - _path: NormalisedURLPath, - _method: HTTPMethod - ): Promise => { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - appInfo: this.getAppInfo(), - }; - if (id === SIGN_UP_API) { - return await signUpAPI(this.apiImpl, options); - } else if (id === SIGN_IN_API) { - return await signInAPI(this.apiImpl, options); - } else if (id === GENERATE_PASSWORD_RESET_TOKEN_API) { - return await generatePasswordResetTokenAPI(this.apiImpl, options); - } else if (id === PASSWORD_RESET_API) { - return await passwordResetAPI(this.apiImpl, options); - } else if (id === SIGNUP_EMAIL_EXISTS_API) { - return await emailExistsAPI(this.apiImpl, options); - } - return false; - }; - - handleError = async (err: STError, _request: BaseRequest, response: BaseResponse): Promise => { - if (err.fromRecipe === Recipe.RECIPE_ID) { - if (err.type === STError.FIELD_ERROR) { - return send200Response(response, { - status: "FIELD_ERROR", - formFields: err.payload, - }); - } else { - throw err; - } - } else { - throw err; - } - }; - - getAllCORSHeaders = (): string[] => { - return []; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - - // extra instance functions below............... - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - let userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }; -} diff --git a/lib/ts/recipe/emailpassword/recipeImplementation.ts b/lib/ts/recipe/emailpassword/recipeImplementation.ts deleted file mode 100644 index 1f74089b7..000000000 --- a/lib/ts/recipe/emailpassword/recipeImplementation.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { RecipeInterface, User } from "./types"; -import { Querier } from "../../querier"; -import NormalisedURLPath from "../../normalisedURLPath"; - -export default function getRecipeInterface(querier: Querier): RecipeInterface { - return { - signUp: async function ({ - email, - password, - }: { - email: string; - password: string; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/signup"), { - email, - password, - }); - if (response.status === "OK") { - return response; - } else { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } - }, - - signIn: async function ({ - email, - password, - }: { - email: string; - password: string; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/signin"), { - email, - password, - }); - if (response.status === "OK") { - return response; - } else { - return { - status: "WRONG_CREDENTIALS_ERROR", - }; - } - }, - - getUserById: async function ({ userId }: { userId: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - userId, - }); - if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; - } - }, - - getUserByEmail: async function ({ email }: { email: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - email, - }); - if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; - } - }, - - createResetPasswordToken: async function ({ - userId, - }: { - userId: string; - }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/password/reset/token"), { - userId, - }); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } - }, - - resetPasswordUsingToken: async function ({ - token, - newPassword, - }: { - token: string; - newPassword: string; - }): Promise< - | { - status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; - } - | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } - > { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/password/reset"), { - method: "token", - token, - newPassword, - }); - return response; - }, - - updateEmailOrPassword: async function (input: { - userId: string; - email?: string; - password?: string; - }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" }> { - let response = await querier.sendPutRequest(new NormalisedURLPath("/recipe/user"), { - userId: input.userId, - email: input.email, - password: input.password, - }); - if (response.status === "OK") { - return { - status: "OK", - }; - } else if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } - }, - }; -} diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts deleted file mode 100644 index ddc623645..000000000 --- a/lib/ts/recipe/emailpassword/types.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; - -export type TypeNormalisedInput = { - signUpFeature: TypeNormalisedInputSignUp; - signInFeature: TypeNormalisedInputSignIn; - getEmailDeliveryConfig: ( - recipeImpl: RecipeInterface, - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - resetPasswordUsingTokenFeature: TypeNormalisedInputResetPasswordUsingTokenFeature; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeInputFormField = { - id: string; - validate?: (value: any) => Promise; - optional?: boolean; -}; - -export type TypeFormField = { id: string; value: any }; - -export type TypeInputSignUp = { - formFields?: TypeInputFormField[]; -}; - -export type NormalisedFormField = { - id: string; - validate: (value: any) => Promise; - optional: boolean; -}; - -export type TypeNormalisedInputSignUp = { - formFields: NormalisedFormField[]; -}; - -export type TypeNormalisedInputSignIn = { - formFields: NormalisedFormField[]; -}; - -export type TypeInputResetPasswordUsingTokenFeature = { - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise; -}; - -export type TypeNormalisedInputResetPasswordUsingTokenFeature = { - formFieldsForGenerateTokenForm: NormalisedFormField[]; - formFieldsForPasswordResetForm: NormalisedFormField[]; -}; - -export type User = { - id: string; - email: string; - timeJoined: number; -}; - -export type TypeInput = { - signUpFeature?: TypeInputSignUp; - emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type RecipeInterface = { - signUp(input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>; - - signIn(input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>; - - getUserById(input: { userId: string; userContext: any }): Promise; - - getUserByEmail(input: { email: string; userContext: any }): Promise; - - createResetPasswordToken(input: { - userId: string; - userContext: any; - }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; - - resetPasswordUsingToken(input: { - token: string; - newPassword: string; - userContext: any; - }): Promise< - | { - status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; - } - | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } - >; - - updateEmailOrPassword(input: { - userId: string; - email?: string; - password?: string; - userContext: any; - }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" }>; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - appInfo: NormalisedAppinfo; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - emailDelivery: EmailDeliveryIngredient; -}; - -export type APIInterface = { - emailExistsGET: - | undefined - | ((input: { - email: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); - - generatePasswordResetTokenPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - >); - - passwordResetPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - token: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - userId?: string; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - | GeneralErrorResponse - >); - - signInPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - | GeneralErrorResponse - >); - - signUpPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | GeneralErrorResponse - >); -}; - -export type TypeEmailPasswordPasswordResetEmailDeliveryInput = { - type: "PASSWORD_RESET"; - user: { - id: string; - email: string; - }; - passwordResetLink: string; -}; - -export type TypeEmailPasswordEmailDeliveryInput = TypeEmailPasswordPasswordResetEmailDeliveryInput; diff --git a/lib/ts/recipe/emailpassword/utils.ts b/lib/ts/recipe/emailpassword/utils.ts deleted file mode 100644 index 27cade323..000000000 --- a/lib/ts/recipe/emailpassword/utils.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import { - TypeInput, - TypeNormalisedInput, - TypeInputSignUp, - TypeNormalisedInputSignUp, - TypeNormalisedInputSignIn, - TypeNormalisedInputResetPasswordUsingTokenFeature, - NormalisedFormField, - TypeInputFormField, -} from "./types"; -import { NormalisedAppinfo } from "../../types"; -import { FORM_FIELD_EMAIL_ID, FORM_FIELD_PASSWORD_ID } from "./constants"; -import { RecipeInterface, APIInterface } from "./types"; -import BackwardCompatibilityService from "./emaildelivery/services/backwardCompatibility"; - -export function validateAndNormaliseUserInput( - recipeInstance: Recipe, - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput { - let signUpFeature = validateAndNormaliseSignupConfig( - recipeInstance, - appInfo, - config === undefined ? undefined : config.signUpFeature - ); - - let signInFeature = validateAndNormaliseSignInConfig(recipeInstance, appInfo, signUpFeature); - - let resetPasswordUsingTokenFeature = validateAndNormaliseResetPasswordUsingTokenConfig(signUpFeature); - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - function getEmailDeliveryConfig(recipeImpl: RecipeInterface, isInServerlessEnv: boolean) { - let emailService = config?.emailDelivery?.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation which calls our supertokens API - */ - if (emailService === undefined) { - emailService = new BackwardCompatibilityService( - recipeImpl, - appInfo, - isInServerlessEnv, - config?.resetPasswordUsingTokenFeature - ); - } - return { - ...config?.emailDelivery, - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }; - } - return { - signUpFeature, - signInFeature, - resetPasswordUsingTokenFeature, - override, - getEmailDeliveryConfig, - }; -} - -function validateAndNormaliseResetPasswordUsingTokenConfig( - signUpConfig: TypeNormalisedInputSignUp -): TypeNormalisedInputResetPasswordUsingTokenFeature { - let formFieldsForPasswordResetForm: NormalisedFormField[] = signUpConfig.formFields - .filter((filter) => filter.id === FORM_FIELD_PASSWORD_ID) - .map((field) => { - return { - id: field.id, - validate: field.validate, - optional: false, - }; - }); - - let formFieldsForGenerateTokenForm: NormalisedFormField[] = signUpConfig.formFields - .filter((filter) => filter.id === FORM_FIELD_EMAIL_ID) - .map((field) => { - return { - id: field.id, - validate: field.validate, - optional: false, - }; - }); - - return { - formFieldsForPasswordResetForm, - formFieldsForGenerateTokenForm, - }; -} - -function normaliseSignInFormFields(formFields: NormalisedFormField[]) { - return formFields - .filter((filter) => filter.id === FORM_FIELD_EMAIL_ID || filter.id === FORM_FIELD_PASSWORD_ID) - .map((field) => { - return { - id: field.id, - // see issue: https://github.com/supertokens/supertokens-node/issues/36 - validate: field.id === FORM_FIELD_EMAIL_ID ? field.validate : defaultValidator, - optional: false, - }; - }); -} - -function validateAndNormaliseSignInConfig( - _: Recipe, - __: NormalisedAppinfo, - signUpConfig: TypeNormalisedInputSignUp -): TypeNormalisedInputSignIn { - let formFields: NormalisedFormField[] = normaliseSignInFormFields(signUpConfig.formFields); - - return { - formFields, - }; -} - -export function normaliseSignUpFormFields(formFields?: TypeInputFormField[]): NormalisedFormField[] { - let normalisedFormFields: NormalisedFormField[] = []; - if (formFields !== undefined) { - formFields.forEach((field) => { - if (field.id === FORM_FIELD_PASSWORD_ID) { - normalisedFormFields.push({ - id: field.id, - validate: field.validate === undefined ? defaultPasswordValidator : field.validate, - optional: false, - }); - } else if (field.id === FORM_FIELD_EMAIL_ID) { - normalisedFormFields.push({ - id: field.id, - validate: field.validate === undefined ? defaultEmailValidator : field.validate, - optional: false, - }); - } else { - normalisedFormFields.push({ - id: field.id, - validate: field.validate === undefined ? defaultValidator : field.validate, - optional: field.optional === undefined ? false : field.optional, - }); - } - }); - } - if (normalisedFormFields.filter((field) => field.id === FORM_FIELD_PASSWORD_ID).length === 0) { - // no password field give by user - normalisedFormFields.push({ - id: FORM_FIELD_PASSWORD_ID, - validate: defaultPasswordValidator, - optional: false, - }); - } - if (normalisedFormFields.filter((field) => field.id === FORM_FIELD_EMAIL_ID).length === 0) { - // no email field give by user - normalisedFormFields.push({ - id: FORM_FIELD_EMAIL_ID, - validate: defaultEmailValidator, - optional: false, - }); - } - return normalisedFormFields; -} - -function validateAndNormaliseSignupConfig( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInputSignUp -): TypeNormalisedInputSignUp { - let formFields: NormalisedFormField[] = normaliseSignUpFormFields( - config === undefined ? undefined : config.formFields - ); - - return { - formFields, - }; -} - -async function defaultValidator(_: any): Promise { - return undefined; -} - -export async function defaultPasswordValidator(value: any) { - // length >= 8 && < 100 - // must have a number and a character - // as per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - - if (typeof value !== "string") { - return "Development bug: Please make sure the password field yields a string"; - } - - if (value.length < 8) { - return "Password must contain at least 8 characters, including a number"; - } - - if (value.length >= 100) { - return "Password's length must be lesser than 100 characters"; - } - - if (value.match(/^.*[A-Za-z]+.*$/) === null) { - return "Password must contain at least one alphabet"; - } - - if (value.match(/^.*[0-9]+.*$/) === null) { - return "Password must contain at least one number"; - } - - return undefined; -} - -export async function defaultEmailValidator(value: any) { - // We check if the email syntax is correct - // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - // Regex from https://stackoverflow.com/a/46181/3867175 - - if (typeof value !== "string") { - return "Development bug: Please make sure the email field yields a string"; - } - - if ( - value.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) === null - ) { - return "Email is invalid"; - } - - return undefined; -} diff --git a/lib/ts/recipe/emailverification/api/emailVerify.ts b/lib/ts/recipe/emailverification/api/emailVerify.ts deleted file mode 100644 index f9b0af318..000000000 --- a/lib/ts/recipe/emailverification/api/emailVerify.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response, normaliseHttpMethod } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; -import Session from "../../session"; - -export default async function emailVerify(apiImplementation: APIInterface, options: APIOptions): Promise { - let result; - - const userContext = makeDefaultUserContextFromAPI(options.req); - - if (normaliseHttpMethod(options.req.getMethod()) === "post") { - // Logic according to Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 - - if (apiImplementation.verifyEmailPOST === undefined) { - return false; - } - - let token = (await options.req.getJSONBody()).token; - if (token === undefined || token === null) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the email verification token", - }); - } - if (typeof token !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "The email verification token must be a string", - }); - } - - const session = await Session.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [], sessionRequired: false }, - userContext - ); - - let response = await apiImplementation.verifyEmailPOST({ - token, - options, - session, - userContext, - }); - if (response.status === "OK") { - result = { status: "OK" }; - } else { - result = response; - } - } else { - if (apiImplementation.isEmailVerifiedGET === undefined) { - return false; - } - - const session = await Session.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [] }, - userContext - ); - result = await apiImplementation.isEmailVerifiedGET({ - options, - session: session!, - userContext, - }); - } - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts b/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts deleted file mode 100644 index b74ecf1cc..000000000 --- a/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; -import Session from "../../session"; - -export default async function generateEmailVerifyToken( - apiImplementation: APIInterface, - options: APIOptions -): Promise { - // Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 - - if (apiImplementation.generateEmailVerifyTokenPOST === undefined) { - return false; - } - const userContext = makeDefaultUserContextFromAPI(options.req); - const session = await Session.getSession( - options.req, - options.res, - { overrideGlobalClaimValidators: () => [] }, - userContext - ); - - const result = await apiImplementation.generateEmailVerifyTokenPOST({ - options, - session: session!, - userContext, - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/emailverification/api/implementation.ts b/lib/ts/recipe/emailverification/api/implementation.ts deleted file mode 100644 index 4cb9e4bdc..000000000 --- a/lib/ts/recipe/emailverification/api/implementation.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { APIInterface, User } from "../"; -import { logDebugMessage } from "../../../logger"; -import EmailVerificationRecipe from "../recipe"; -import { GeneralErrorResponse } from "../../../types"; -import { EmailVerificationClaim } from "../emailVerificationClaim"; -import SessionError from "../../session/error"; -import { getEmailVerifyLink } from "../utils"; - -export default function getAPIInterface(): APIInterface { - return { - verifyEmailPOST: async function ({ - token, - options, - session, - userContext, - }): Promise< - { status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse - > { - const res = await options.recipeImplementation.verifyEmailUsingToken({ token, userContext }); - - if (res.status === "OK" && session !== undefined) { - try { - await session.fetchAndSetClaim(EmailVerificationClaim, userContext); - } catch (err) { - // This should never happen, since we've just set the status above. - if ((err as Error).message === "UNKNOWN_USER_ID") { - throw new SessionError({ - type: SessionError.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - throw err; - } - } - return res; - }, - - isEmailVerifiedGET: async function ({ - userContext, - session, - }): Promise< - | { - status: "OK"; - isVerified: boolean; - } - | GeneralErrorResponse - > { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); - } - - try { - await session.fetchAndSetClaim(EmailVerificationClaim, userContext); - } catch (err) { - if ((err as Error).message === "UNKNOWN_USER_ID") { - throw new SessionError({ - type: SessionError.UNAUTHORISED, - message: "Unknown User ID provided", - }); - } - throw err; - } - const isVerified = await session.getClaimValue(EmailVerificationClaim, userContext); - - if (isVerified === undefined) { - throw new Error("Should never come here: EmailVerificationClaim failed to set value"); - } - - return { - status: "OK", - isVerified, - }; - }, - - generateEmailVerifyTokenPOST: async function ({ - options, - userContext, - session, - }): Promise<{ status: "OK" | "EMAIL_ALREADY_VERIFIED_ERROR" } | GeneralErrorResponse> { - if (session === undefined) { - throw new Error("Session is undefined. Should not come here."); - } - - const userId = session.getUserId(); - - const emailInfo = await EmailVerificationRecipe.getInstanceOrThrowError().getEmailForUserId( - userId, - userContext - ); - - if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - logDebugMessage( - `Email verification email not sent to user ${userId} because it doesn't have an email address.` - ); - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } else if (emailInfo.status === "OK") { - let response = await options.recipeImplementation.createEmailVerificationToken({ - userId, - email: emailInfo.email, - userContext, - }); - - if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - if ((await session.getClaimValue(EmailVerificationClaim)) !== true) { - // this can happen if the email was verified in another browser - // and this session is still outdated - and the user has not - // called the get email verification API yet. - await session.fetchAndSetClaim(EmailVerificationClaim, userContext); - } - logDebugMessage( - `Email verification email not sent to ${emailInfo.email} because it is already verified.` - ); - return response; - } - - if ((await session.getClaimValue(EmailVerificationClaim)) !== false) { - // this can happen if the email was unverified in another browser - // and this session is still outdated - and the user has not - // called the get email verification API yet. - await session.fetchAndSetClaim(EmailVerificationClaim, userContext); - } - - let emailVerifyLink = getEmailVerifyLink({ - appInfo: options.appInfo, - token: response.token, - recipeId: options.recipeId, - }); - - logDebugMessage(`Sending email verification email to ${emailInfo}`); - await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "EMAIL_VERIFICATION", - user: { - id: userId, - email: emailInfo.email, - }, - emailVerifyLink, - userContext, - }); - - return { - status: "OK", - }; - } else { - throw new SessionError({ type: SessionError.UNAUTHORISED, message: "Unknown User ID provided" }); - } - }, - }; -} diff --git a/lib/ts/recipe/emailverification/constants.ts b/lib/ts/recipe/emailverification/constants.ts deleted file mode 100644 index 0f1f4acc2..000000000 --- a/lib/ts/recipe/emailverification/constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const GENERATE_EMAIL_VERIFY_TOKEN_API = "/user/email/verify/token"; - -export const EMAIL_VERIFY_API = "/user/email/verify"; diff --git a/lib/ts/recipe/emailverification/emailVerificationClaim.ts b/lib/ts/recipe/emailverification/emailVerificationClaim.ts deleted file mode 100644 index 0ef14d5ce..000000000 --- a/lib/ts/recipe/emailverification/emailVerificationClaim.ts +++ /dev/null @@ -1,51 +0,0 @@ -import EmailVerificationRecipe from "./recipe"; -import { BooleanClaim } from "../session/claims"; -import { SessionClaimValidator } from "../session"; - -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -export class EmailVerificationClaimClass extends BooleanClaim { - constructor() { - super({ - key: "st-ev", - async fetchValue(userId, userContext) { - const recipe = EmailVerificationRecipe.getInstanceOrThrowError(); - let emailInfo = await recipe.getEmailForUserId(userId, userContext); - - if (emailInfo.status === "OK") { - return recipe.recipeInterfaceImpl.isEmailVerified({ userId, email: emailInfo.email, userContext }); - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // We consider people without email addresses as validated - return true; - } else { - throw new Error("UNKNOWN_USER_ID"); - } - }, - defaultMaxAgeInSeconds: 300, - }); - - this.validators = { - ...this.validators, - isVerified: (refetchTimeOnFalseInSeconds: number = 10, maxAgeInSeconds: number = 300) => ({ - ...this.validators.hasValue(true, maxAgeInSeconds), - shouldRefetch: (payload, userContext) => { - const value = this.getValueFromPayload(payload, userContext); - return ( - value === undefined || - this.getLastRefetchTime(payload, userContext)! < Date.now() - maxAgeInSeconds * 1000 || - (value === false && - this.getLastRefetchTime(payload, userContext)! < - Date.now() - refetchTimeOnFalseInSeconds * 1000) - ); - }, - }), - }; - } - - validators!: BooleanClaim["validators"] & { - isVerified: (refetchTimeOnFalseInSeconds?: number, maxAgeInSeconds?: number) => SessionClaimValidator; - }; -} - -export const EmailVerificationClaim = new EmailVerificationClaimClass(); diff --git a/lib/ts/recipe/emailverification/emailVerificationFunctions.ts b/lib/ts/recipe/emailverification/emailVerificationFunctions.ts deleted file mode 100644 index edc069713..000000000 --- a/lib/ts/recipe/emailverification/emailVerificationFunctions.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { User } from "./types"; -import { NormalisedAppinfo } from "../../types"; -import axios, { AxiosError } from "axios"; -import { logDebugMessage } from "../../logger"; - -export function createAndSendCustomEmail(appInfo: NormalisedAppinfo) { - return async (user: User, emailVerifyURLWithToken: string) => { - if (process.env.TEST_MODE === "testing") { - return; - } - try { - await axios({ - method: "POST", - url: "https://api.supertokens.io/0/st/auth/email/verify", - data: { - email: user.email, - appName: appInfo.appName, - emailVerifyURL: emailVerifyURLWithToken, - }, - headers: { - "api-version": 0, - }, - }); - logDebugMessage(`Email sent to ${user.email}`); - } catch (error) { - logDebugMessage("Error sending verification email"); - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - if (err.response) { - logDebugMessage(`Error status: ${err.response.status}`); - logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logDebugMessage(`Error: ${err.message}`); - } - } else { - logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logDebugMessage("Logging the input below:"); - logDebugMessage( - JSON.stringify( - { - email: user.email, - appName: appInfo.appName, - emailVerifyURL: emailVerifyURLWithToken, - }, - null, - 2 - ) - ); - } - }; -} diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts deleted file mode 100644 index c9b55d7aa..000000000 --- a/lib/ts/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeEmailVerificationEmailDeliveryInput, User } from "../../../types"; -import { createAndSendCustomEmail as defaultCreateAndSendCustomEmail } from "../../../emailVerificationFunctions"; -import { NormalisedAppinfo } from "../../../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; - -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private appInfo: NormalisedAppinfo; - private isInServerlessEnv: boolean; - private createAndSendCustomEmail: ( - user: User, - emailVerificationURLWithToken: string, - userContext: any - ) => Promise; - - constructor( - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - createAndSendCustomEmail?: ( - user: User, - emailVerificationURLWithToken: string, - userContext: any - ) => Promise - ) { - this.appInfo = appInfo; - this.isInServerlessEnv = isInServerlessEnv; - this.createAndSendCustomEmail = - createAndSendCustomEmail === undefined - ? defaultCreateAndSendCustomEmail(this.appInfo) - : createAndSendCustomEmail; - } - - sendEmail = async (input: TypeEmailVerificationEmailDeliveryInput & { userContext: any }) => { - try { - if (!this.isInServerlessEnv) { - this.createAndSendCustomEmail(input.user, input.emailVerifyLink, input.userContext).catch((_) => {}); - } else { - // see https://github.com/supertokens/supertokens-node/pull/135 - await this.createAndSendCustomEmail(input.user, input.emailVerifyLink, input.userContext); - } - } catch (_) {} - }; -} diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/index.ts b/lib/ts/recipe/emailverification/emaildelivery/services/index.ts deleted file mode 100644 index d70e3f387..000000000 --- a/lib/ts/recipe/emailverification/emaildelivery/services/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import SMTP from "./smtp"; -export let SMTPService = SMTP; diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/emailVerify.ts b/lib/ts/recipe/emailverification/emaildelivery/services/smtp/emailVerify.ts deleted file mode 100644 index c9dd89285..000000000 --- a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/emailVerify.ts +++ /dev/null @@ -1,941 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/emaildelivery/services/smtp"; -import Supertokens from "../../../../../supertokens"; - -export default function getEmailVerifyEmailContent(input: TypeEmailVerificationEmailDeliveryInput): GetContentResult { - let supertokens = Supertokens.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getEmailVerifyEmailHTML(appName, input.user.email, input.emailVerifyLink); - return { - body, - toEmail: input.user.email, - subject: "Email verification instructions", - isHtml: true, - }; -} - -export function getEmailVerifyEmailHTML(appName: string, email: string, verificationLink: string) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
- - -
-
- -

- Please verify your email address for ${appName} - by clicking the button below.

- - -
-
-

- Alternatively, you can directly paste this link - in your browser
- ${verificationLink} -

-
-
- - - - -
-
- -
- - - - - -
- - - - - - -
- - -

- This email is meant for ${email} -

-
-
- -
- -
-
- - - - `; -} diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/emailverification/emaildelivery/services/smtp/index.ts deleted file mode 100644 index 6adb2549e..000000000 --- a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; -import { createTransport } from "nodemailer"; -import OverrideableBuilder from "supertokens-js-override"; -import { getServiceImplementation } from "./serviceImplementation"; - -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - - constructor(config: TypeInput) { - const transporter = createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } - - sendEmail = async (input: TypeEmailVerificationEmailDeliveryInput & { userContext: any }) => { - let content = await this.serviceImpl.getContent(input); - await this.serviceImpl.sendRawEmail({ - ...content, - userContext: input.userContext, - }); - }; -} diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts b/lib/ts/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts deleted file mode 100644 index 470415f0c..000000000 --- a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; -import { Transporter } from "nodemailer"; -import { - ServiceInterface, - TypeInputSendRawEmail, - GetContentResult, -} from "../../../../../ingredients/emaildelivery/services/smtp"; -import getEmailVerifyEmailContent from "./emailVerify"; - -export function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface { - return { - sendRawEmail: async function (input: TypeInputSendRawEmail) { - if (input.isHtml) { - await transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - await transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }, - getContent: async function ( - input: TypeEmailVerificationEmailDeliveryInput & { userContext: any } - ): Promise { - return getEmailVerifyEmailContent(input); - }, - }; -} diff --git a/lib/ts/recipe/emailverification/error.ts b/lib/ts/recipe/emailverification/error.ts deleted file mode 100644 index dbe076b58..000000000 --- a/lib/ts/recipe/emailverification/error.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; - -export default class SessionError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }) { - super({ - ...options, - }); - this.fromRecipe = "emailverification"; - } -} diff --git a/lib/ts/recipe/emailverification/index.ts b/lib/ts/recipe/emailverification/index.ts deleted file mode 100644 index 3953d8dd6..000000000 --- a/lib/ts/recipe/emailverification/index.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { RecipeInterface, APIOptions, APIInterface, User, TypeEmailVerificationEmailDeliveryInput } from "./types"; -import { EmailVerificationClaim } from "./emailVerificationClaim"; - -export default class Wrapper { - static init = Recipe.init; - - static Error = SuperTokensError; - - static EmailVerificationClaim = EmailVerificationClaim; - - static async createEmailVerificationToken( - userId: string, - email?: string, - userContext?: any - ): Promise< - | { - status: "OK"; - token: string; - } - | { status: "EMAIL_ALREADY_VERIFIED_ERROR" } - > { - const recipeInstance = Recipe.getInstanceOrThrowError(); - - if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - - return await recipeInstance.recipeInterfaceImpl.createEmailVerificationToken({ - userId, - email: email!, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async verifyEmailUsingToken(token: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ - token, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async isEmailVerified(userId: string, email?: string, userContext?: any) { - const recipeInstance = Recipe.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); - - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - return true; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - - return await recipeInstance.recipeInterfaceImpl.isEmailVerified({ - userId, - email: email!, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async revokeEmailVerificationTokens(userId: string, email?: string, userContext?: any) { - const recipeInstance = Recipe.getInstanceOrThrowError(); - - // If the dev wants to delete the tokens for an old email address of the user they can pass the address - // but redeeming those tokens would have no effect on isEmailVerified called without the old address - // so in general that is not necessary either. - if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // This only happens for phone based passwordless users (or if the user added a custom getEmailForUserId) - // We can return OK here, since there is no way to create an email verification token - // if getEmailForUserId returns EMAIL_DOES_NOT_EXIST_ERROR. - return { - status: "OK", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - - return await recipeInstance.recipeInterfaceImpl.revokeEmailVerificationTokens({ - userId, - email: email!, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async unverifyEmail(userId: string, email?: string, userContext?: any) { - const recipeInstance = Recipe.getInstanceOrThrowError(); - if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext); - if (emailInfo.status === "OK") { - email = emailInfo.email; - } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { - // Here we are returning OK since that's how it used to work, but a later call to isVerified will still return true - return { - status: "OK", - }; - } else { - throw new global.Error("Unknown User ID provided without email"); - } - } - return await recipeInstance.recipeInterfaceImpl.unverifyEmail({ - userId, - email: email!, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: any }) { - let recipeInstance = Recipe.getInstanceOrThrowError(); - return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, - ...input, - }); - } -} - -export let init = Wrapper.init; - -export let Error = Wrapper.Error; - -export let createEmailVerificationToken = Wrapper.createEmailVerificationToken; - -export let verifyEmailUsingToken = Wrapper.verifyEmailUsingToken; - -export let isEmailVerified = Wrapper.isEmailVerified; - -export let revokeEmailVerificationTokens = Wrapper.revokeEmailVerificationTokens; - -export let unverifyEmail = Wrapper.unverifyEmail; - -export type { RecipeInterface, APIOptions, APIInterface, User }; - -export let sendEmail = Wrapper.sendEmail; - -export { EmailVerificationClaim } from "./emailVerificationClaim"; diff --git a/lib/ts/recipe/emailverification/recipe.ts b/lib/ts/recipe/emailverification/recipe.ts deleted file mode 100644 index 22b29bc88..000000000 --- a/lib/ts/recipe/emailverification/recipe.ts +++ /dev/null @@ -1,212 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForUserIdFunc } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import STError from "./error"; -import { validateAndNormaliseUserInput } from "./utils"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { GENERATE_EMAIL_VERIFY_TOKEN_API, EMAIL_VERIFY_API } from "./constants"; -import generateEmailVerifyTokenAPI from "./api/generateEmailVerifyToken"; -import emailVerifyAPI from "./api/emailVerify"; -import RecipeImplementation from "./recipeImplementation"; -import APIImplementation from "./api/implementation"; -import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { TypeEmailVerificationEmailDeliveryInput } from "./types"; -import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; -import SessionRecipe from "../session/recipe"; -import { EmailVerificationClaim } from "./emailVerificationClaim"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "emailverification"; - - config: TypeNormalisedInput; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - emailDelivery: EmailDeliveryIngredient; - - getEmailForUserIdFuncsFromOtherRecipes: GetEmailForUserIdFunc[] = []; - - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - } - ) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - - { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new EmailDeliveryIngredient(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) - : ingredients.emailDelivery; - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static getInstance(): Recipe | undefined { - return Recipe.instance; - } - - static init(config: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { - emailDelivery: undefined, - }); - - PostSuperTokensInitCallbacks.addPostInitCallback(() => { - SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(EmailVerificationClaim); - - if (config.mode === "REQUIRED") { - SessionRecipe.getInstanceOrThrowError().addClaimValidatorFromOtherRecipe( - EmailVerificationClaim.validators.isVerified() - ); - } - }); - - return Recipe.instance; - } else { - throw new Error( - "Emailverification recipe has already been initialised. Please check your code for bugs." - ); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - // abstract instance functions below............... - - getAPIsHandled = (): APIHandled[] => { - return [ - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(GENERATE_EMAIL_VERIFY_TOKEN_API), - id: GENERATE_EMAIL_VERIFY_TOKEN_API, - disabled: this.apiImpl.generateEmailVerifyTokenPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(EMAIL_VERIFY_API), - id: EMAIL_VERIFY_API, - disabled: this.apiImpl.verifyEmailPOST === undefined, - }, - { - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(EMAIL_VERIFY_API), - id: EMAIL_VERIFY_API, - disabled: this.apiImpl.isEmailVerifiedGET === undefined, - }, - ]; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - _: NormalisedURLPath, - __: HTTPMethod - ): Promise => { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - appInfo: this.getAppInfo(), - }; - if (id === GENERATE_EMAIL_VERIFY_TOKEN_API) { - return await generateEmailVerifyTokenAPI(this.apiImpl, options); - } else { - return await emailVerifyAPI(this.apiImpl, options); - } - }; - - handleError = async (err: STError, _: BaseRequest, __: BaseResponse): Promise => { - throw err; - }; - - getAllCORSHeaders = (): string[] => { - return []; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - if (this.config.getEmailForUserId !== undefined) { - const userRes = await this.config.getEmailForUserId(userId, userContext); - if (userRes.status !== "UNKNOWN_USER_ID_ERROR") { - return userRes; - } - } - - for (const getEmailForUserId of this.getEmailForUserIdFuncsFromOtherRecipes) { - const res = await getEmailForUserId(userId, userContext); - if (res.status !== "UNKNOWN_USER_ID_ERROR") { - return res; - } - } - - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }; - - addGetEmailForUserIdFunc = (func: GetEmailForUserIdFunc): void => { - this.getEmailForUserIdFuncsFromOtherRecipes.push(func); - }; -} diff --git a/lib/ts/recipe/emailverification/recipeImplementation.ts b/lib/ts/recipe/emailverification/recipeImplementation.ts deleted file mode 100644 index 547c71890..000000000 --- a/lib/ts/recipe/emailverification/recipeImplementation.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { RecipeInterface, User } from "./"; -import { Querier } from "../../querier"; -import NormalisedURLPath from "../../normalisedURLPath"; - -export default function getRecipeInterface(querier: Querier): RecipeInterface { - return { - createEmailVerificationToken: async function ({ - userId, - email, - }: { - userId: string; - email: string; - }): Promise< - | { - status: "OK"; - token: string; - } - | { status: "EMAIL_ALREADY_VERIFIED_ERROR" } - > { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/email/verify/token"), { - userId, - email, - }); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "EMAIL_ALREADY_VERIFIED_ERROR", - }; - } - }, - - verifyEmailUsingToken: async function ({ - token, - }: { - token: string; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/email/verify"), { - method: "token", - token, - }); - if (response.status === "OK") { - return { - status: "OK", - user: { - id: response.userId, - email: response.email, - }, - }; - } else { - return { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR", - }; - } - }, - - isEmailVerified: async function ({ userId, email }: { userId: string; email: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user/email/verify"), { - userId, - email, - }); - return response.isVerified; - }, - - revokeEmailVerificationTokens: async function (input: { - userId: string; - email: string; - }): Promise<{ status: "OK" }> { - await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/email/verify/token/remove"), { - userId: input.userId, - email: input.email, - }); - return { status: "OK" }; - }, - - unverifyEmail: async function (input: { userId: string; email: string }): Promise<{ status: "OK" }> { - await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/email/verify/remove"), { - userId: input.userId, - email: input.email, - }); - return { status: "OK" }; - }, - }; -} diff --git a/lib/ts/recipe/emailverification/types.ts b/lib/ts/recipe/emailverification/types.ts deleted file mode 100644 index 55360c3f9..000000000 --- a/lib/ts/recipe/emailverification/types.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; -import { SessionContainerInterface } from "../session/types"; - -export type TypeInput = { - mode: "REQUIRED" | "OPTIONAL"; - emailDelivery?: EmailDeliveryTypeInput; - getEmailForUserId?: ( - userId: string, - userContext: any - ) => Promise< - | { - status: "OK"; - email: string; - } - | { status: "EMAIL_DOES_NOT_EXIST_ERROR" | "UNKNOWN_USER_ID_ERROR" } - >; - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: (user: User, emailVerificationURLWithToken: string, userContext: any) => Promise; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - mode: "REQUIRED" | "OPTIONAL"; - getEmailDeliveryConfig: ( - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - getEmailForUserId?: ( - userId: string, - userContext: any - ) => Promise< - | { - status: "OK"; - email: string; - } - | { status: "EMAIL_DOES_NOT_EXIST_ERROR" | "UNKNOWN_USER_ID_ERROR" } - >; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type User = { - id: string; - email: string; -}; - -export type RecipeInterface = { - createEmailVerificationToken(input: { - userId: string; - email: string; - userContext: any; - }): Promise< - | { - status: "OK"; - token: string; - } - | { status: "EMAIL_ALREADY_VERIFIED_ERROR" } - >; - - verifyEmailUsingToken(input: { - token: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>; - - isEmailVerified(input: { userId: string; email: string; userContext: any }): Promise; - - revokeEmailVerificationTokens(input: { - userId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK" }>; - - unverifyEmail(input: { userId: string; email: string; userContext: any }): Promise<{ status: "OK" }>; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - appInfo: NormalisedAppinfo; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - emailDelivery: EmailDeliveryIngredient; -}; - -export type APIInterface = { - verifyEmailPOST: - | undefined - | ((input: { - token: string; - options: APIOptions; - userContext: any; - session?: SessionContainerInterface; - }) => Promise< - { status: "OK"; user: User } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse - >); - - isEmailVerifiedGET: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - session: SessionContainerInterface; - }) => Promise< - | { - status: "OK"; - isVerified: boolean; - } - | GeneralErrorResponse - >); - - generateEmailVerifyTokenPOST: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - session: SessionContainerInterface; - }) => Promise<{ status: "EMAIL_ALREADY_VERIFIED_ERROR" | "OK" } | GeneralErrorResponse>); -}; - -export type TypeEmailVerificationEmailDeliveryInput = { - type: "EMAIL_VERIFICATION"; - user: { - id: string; - email: string; - }; - emailVerifyLink: string; -}; - -export type GetEmailForUserIdFunc = ( - userId: string, - userContext: any -) => Promise< - | { - status: "OK"; - email: string; - } - | { status: "EMAIL_DOES_NOT_EXIST_ERROR" | "UNKNOWN_USER_ID_ERROR" } ->; diff --git a/lib/ts/recipe/emailverification/utils.ts b/lib/ts/recipe/emailverification/utils.ts deleted file mode 100644 index e99d5b4f3..000000000 --- a/lib/ts/recipe/emailverification/utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo } from "../../types"; -import BackwardCompatibilityService from "./emaildelivery/services/backwardCompatibility"; - -export function validateAndNormaliseUserInput( - _: Recipe, - appInfo: NormalisedAppinfo, - config: TypeInput -): TypeNormalisedInput { - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config.override, - }; - - function getEmailDeliveryConfig(isInServerlessEnv: boolean) { - let emailService = config.emailDelivery?.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation which calls our supertokens API - */ - if (emailService === undefined) { - emailService = new BackwardCompatibilityService( - appInfo, - isInServerlessEnv, - config.createAndSendCustomEmail - ); - } - return { - ...config.emailDelivery, - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }; - } - return { - mode: config.mode, - getEmailForUserId: config.getEmailForUserId, - override, - getEmailDeliveryConfig, - }; -} - -export function getEmailVerifyLink(input: { appInfo: NormalisedAppinfo; token: string; recipeId: string }): string { - return ( - input.appInfo.websiteDomain.getAsStringDangerous() + - input.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify-email" + - "?token=" + - input.token + - "&rid=" + - input.recipeId - ); -} diff --git a/lib/ts/recipe/jwt/api/getJWKS.ts b/lib/ts/recipe/jwt/api/getJWKS.ts deleted file mode 100644 index 81f38fddd..000000000 --- a/lib/ts/recipe/jwt/api/getJWKS.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { APIInterface, APIOptions } from "../types"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function getJWKS(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.getJWKSGET === undefined) { - return false; - } - - let result = await apiImplementation.getJWKSGET({ - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - options.res.setHeader("Access-Control-Allow-Origin", "*", false); - send200Response(options.res, { keys: result.keys }); - } else { - send200Response(options.res, result); - } - return true; -} diff --git a/lib/ts/recipe/jwt/api/implementation.ts b/lib/ts/recipe/jwt/api/implementation.ts deleted file mode 100644 index e6a48a9e6..000000000 --- a/lib/ts/recipe/jwt/api/implementation.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { APIInterface, APIOptions, JsonWebKey } from "../types"; -import { GeneralErrorResponse } from "../../../types"; - -export default function getAPIImplementation(): APIInterface { - return { - getJWKSGET: async function ({ - options, - userContext, - }: { - options: APIOptions; - userContext: any; - }): Promise<{ status: "OK"; keys: JsonWebKey[] } | GeneralErrorResponse> { - return await options.recipeImplementation.getJWKS({ userContext }); - }, - }; -} diff --git a/lib/ts/recipe/jwt/constants.ts b/lib/ts/recipe/jwt/constants.ts deleted file mode 100644 index 7f30528df..000000000 --- a/lib/ts/recipe/jwt/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const GET_JWKS_API = "/jwt/jwks.json"; diff --git a/lib/ts/recipe/jwt/index.ts b/lib/ts/recipe/jwt/index.ts deleted file mode 100644 index 147e7ce0e..000000000 --- a/lib/ts/recipe/jwt/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import { APIInterface, RecipeInterface, APIOptions, JsonWebKey } from "./types"; - -export default class Wrapper { - static init = Recipe.init; - - static async createJWT(payload: any, validitySeconds?: number, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ - payload, - validitySeconds, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async getJWKS(userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, - }); - } -} - -export let init = Wrapper.init; -export let createJWT = Wrapper.createJWT; -export let getJWKS = Wrapper.getJWKS; - -export type { APIInterface, APIOptions, RecipeInterface, JsonWebKey }; diff --git a/lib/ts/recipe/jwt/recipe.ts b/lib/ts/recipe/jwt/recipe.ts deleted file mode 100644 index ba40ff59a..000000000 --- a/lib/ts/recipe/jwt/recipe.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import SuperTokensError from "../../error"; -import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLPath from "../../normalisedURLPath"; -import normalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import getJWKS from "./api/getJWKS"; -import APIImplementation from "./api/implementation"; -import { GET_JWKS_API } from "./constants"; -import RecipeImplementation from "./recipeImplementation"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import OverrideableBuilder from "supertokens-js-override"; - -export default class Recipe extends RecipeModule { - static RECIPE_ID = "jwt"; - private static instance: Recipe | undefined = undefined; - - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - apiImpl: APIInterface; - isInServerlessEnv: boolean; - - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - - { - let builder = new OverrideableBuilder( - RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId), this.config, appInfo) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - - /* Init functions */ - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("JWT recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - /* RecipeModule functions */ - - getAPIsHandled(): APIHandled[] { - return [ - { - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(GET_JWKS_API), - id: GET_JWKS_API, - disabled: this.apiImpl.getJWKSGET === undefined, - }, - ]; - } - - handleAPIRequest = async ( - _: string, - req: BaseRequest, - res: BaseResponse, - __: normalisedURLPath, - ___: HTTPMethod - ): Promise => { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - }; - - return await getJWKS(this.apiImpl, options); - }; - - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise { - throw error; - } - - getAllCORSHeaders(): string[] { - return []; - } - - isErrorFromThisRecipe(err: any): err is error { - return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - } -} diff --git a/lib/ts/recipe/jwt/recipeImplementation.ts b/lib/ts/recipe/jwt/recipeImplementation.ts deleted file mode 100644 index 5e7ce2e47..000000000 --- a/lib/ts/recipe/jwt/recipeImplementation.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import NormalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; -import { NormalisedAppinfo } from "../../types"; -import { JsonWebKey, RecipeInterface, TypeNormalisedInput } from "./types"; - -export default function getRecipeInterface( - querier: Querier, - config: TypeNormalisedInput, - appInfo: NormalisedAppinfo -): RecipeInterface { - return { - createJWT: async function ({ - payload, - validitySeconds, - }: { - payload?: any; - validitySeconds?: number; - }): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - > { - if (validitySeconds === undefined) { - // If the user does not provide a validity to this function and the config validity is also undefined, use 100 years (in seconds) - validitySeconds = config.jwtValiditySeconds; - } - - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/jwt"), { - payload: payload ?? {}, - validity: validitySeconds, - algorithm: "RS256", - jwksDomain: appInfo.apiDomain.getAsStringDangerous(), - }); - - if (response.status === "OK") { - return { - status: "OK", - jwt: response.jwt, - }; - } else { - return { - status: "UNSUPPORTED_ALGORITHM_ERROR", - }; - } - }, - - getJWKS: async function (): Promise<{ status: "OK"; keys: JsonWebKey[] }> { - return await querier.sendGetRequest(new NormalisedURLPath("/recipe/jwt/jwks"), {}); - }, - }; -} diff --git a/lib/ts/recipe/jwt/types.ts b/lib/ts/recipe/jwt/types.ts deleted file mode 100644 index 49b7fb57c..000000000 --- a/lib/ts/recipe/jwt/types.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse } from "../../types"; - -export type JsonWebKey = { - kty: string; - kid: string; - n: string; - e: string; - alg: string; - use: string; -}; - -export type TypeInput = { - jwtValiditySeconds?: number; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - jwtValiditySeconds: number; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; -}; - -export type RecipeInterface = { - createJWT(input: { - payload?: any; - validitySeconds?: number; - userContext: any; - }): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - - getJWKS(input: { - userContext: any; - }): Promise<{ - status: "OK"; - keys: JsonWebKey[]; - }>; -}; - -export type APIInterface = { - getJWKSGET: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - }) => Promise<{ status: "OK"; keys: JsonWebKey[] } | GeneralErrorResponse>); -}; diff --git a/lib/ts/recipe/jwt/utils.ts b/lib/ts/recipe/jwt/utils.ts deleted file mode 100644 index a891fa0d5..000000000 --- a/lib/ts/recipe/jwt/utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { NormalisedAppinfo } from "../../types"; -import Recipe from "./recipe"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; - -export function validateAndNormaliseUserInput( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput { - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - return { - jwtValiditySeconds: config?.jwtValiditySeconds ?? 3153600000, - override, - }; -} diff --git a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts deleted file mode 100644 index 825960bb5..000000000 --- a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { send200Response } from "../../../utils"; -import { APIInterface, APIOptions } from "../types"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function getOpenIdDiscoveryConfiguration( - apiImplementation: APIInterface, - options: APIOptions -): Promise { - if (apiImplementation.getOpenIdDiscoveryConfigurationGET === undefined) { - return false; - } - - let result = await apiImplementation.getOpenIdDiscoveryConfigurationGET({ - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - if (result.status === "OK") { - options.res.setHeader("Access-Control-Allow-Origin", "*", false); - send200Response(options.res, { - issuer: result.issuer, - jwks_uri: result.jwks_uri, - }); - } else { - send200Response(options.res, result); - } - return true; -} diff --git a/lib/ts/recipe/openid/api/implementation.ts b/lib/ts/recipe/openid/api/implementation.ts deleted file mode 100644 index 19bf1a4b6..000000000 --- a/lib/ts/recipe/openid/api/implementation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { APIInterface, APIOptions } from "../types"; -import { GeneralErrorResponse } from "../../../types"; - -export default function getAPIImplementation(): APIInterface { - return { - getOpenIdDiscoveryConfigurationGET: async function ({ - options, - userContext, - }: { - options: APIOptions; - userContext: any; - }): Promise<{ status: "OK"; issuer: string; jwks_uri: string } | GeneralErrorResponse> { - return await options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); - }, - }; -} diff --git a/lib/ts/recipe/openid/constants.ts b/lib/ts/recipe/openid/constants.ts deleted file mode 100644 index 185937b46..000000000 --- a/lib/ts/recipe/openid/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -export const GET_DISCOVERY_CONFIG_URL = "/.well-known/openid-configuration"; diff --git a/lib/ts/recipe/openid/index.ts b/lib/ts/recipe/openid/index.ts deleted file mode 100644 index c90b414a7..000000000 --- a/lib/ts/recipe/openid/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import OpenIdRecipe from "./recipe"; - -export default class OpenIdRecipeWrapper { - static init = OpenIdRecipe.init; - - static getOpenIdDiscoveryConfiguration(userContext?: any) { - return OpenIdRecipe.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ - userContext: userContext === undefined ? {} : userContext, - }); - } - - static createJWT(payload?: any, validitySeconds?: number, userContext?: any) { - return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.createJWT({ - payload, - validitySeconds, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static getJWKS(userContext?: any) { - return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, - }); - } -} - -export let init = OpenIdRecipeWrapper.init; -export let getOpenIdDiscoveryConfiguration = OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration; -export let createJWT = OpenIdRecipeWrapper.createJWT; -export let getJWKS = OpenIdRecipeWrapper.getJWKS; diff --git a/lib/ts/recipe/openid/recipe.ts b/lib/ts/recipe/openid/recipe.ts deleted file mode 100644 index 426d54ad4..000000000 --- a/lib/ts/recipe/openid/recipe.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import STError from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import normalisedURLPath from "../../normalisedURLPath"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { APIInterface, APIOptions, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import JWTRecipe from "../jwt/recipe"; -import OverrideableBuilder from "supertokens-js-override"; -import RecipeImplementation from "./recipeImplementation"; -import APIImplementation from "./api/implementation"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { GET_DISCOVERY_CONFIG_URL } from "./constants"; -import getOpenIdDiscoveryConfiguration from "./api/getOpenIdDiscoveryConfiguration"; - -export default class OpenIdRecipe extends RecipeModule { - static RECIPE_ID = "openid"; - private static instance: OpenIdRecipe | undefined = undefined; - config: TypeNormalisedInput; - jwtRecipe: JWTRecipe; - recipeImplementation: RecipeInterface; - apiImpl: APIInterface; - - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { - super(recipeId, appInfo); - - this.config = validateAndNormaliseUserInput(appInfo, config); - this.jwtRecipe = new JWTRecipe(recipeId, appInfo, isInServerlessEnv, { - jwtValiditySeconds: this.config.jwtValiditySeconds, - override: this.config.override.jwtFeature, - }); - - let builder = new OverrideableBuilder(RecipeImplementation(this.config, this.jwtRecipe.recipeInterfaceImpl)); - - this.recipeImplementation = builder.override(this.config.override.functions).build(); - - let apiBuilder = new OverrideableBuilder(APIImplementation()); - - this.apiImpl = apiBuilder.override(this.config.override.apis).build(); - } - - static getInstanceOrThrowError(): OpenIdRecipe { - if (OpenIdRecipe.instance !== undefined) { - return OpenIdRecipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (OpenIdRecipe.instance === undefined) { - OpenIdRecipe.instance = new OpenIdRecipe(OpenIdRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return OpenIdRecipe.instance; - } else { - throw new Error("OpenId recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - OpenIdRecipe.instance = undefined; - } - - getAPIsHandled = (): APIHandled[] => { - return [ - { - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(GET_DISCOVERY_CONFIG_URL), - id: GET_DISCOVERY_CONFIG_URL, - disabled: this.apiImpl.getOpenIdDiscoveryConfigurationGET === undefined, - }, - ...this.jwtRecipe.getAPIsHandled(), - ]; - }; - handleAPIRequest = async ( - id: string, - req: BaseRequest, - response: BaseResponse, - path: normalisedURLPath, - method: HTTPMethod - ): Promise => { - let apiOptions: APIOptions = { - recipeImplementation: this.recipeImplementation, - config: this.config, - recipeId: this.getRecipeId(), - req, - res: response, - }; - - if (id === GET_DISCOVERY_CONFIG_URL) { - return await getOpenIdDiscoveryConfiguration(this.apiImpl, apiOptions); - } else { - return this.jwtRecipe.handleAPIRequest(id, req, response, path, method); - } - }; - handleError = async (error: STError, request: BaseRequest, response: BaseResponse): Promise => { - if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { - throw error; - } else { - return await this.jwtRecipe.handleError(error, request, response); - } - }; - getAllCORSHeaders = (): string[] => { - return [...this.jwtRecipe.getAllCORSHeaders()]; - }; - isErrorFromThisRecipe = (err: any): err is STError => { - return ( - (STError.isErrorFromSuperTokens(err) && err.fromRecipe === OpenIdRecipe.RECIPE_ID) || - this.jwtRecipe.isErrorFromThisRecipe(err) - ); - }; -} diff --git a/lib/ts/recipe/openid/recipeImplementation.ts b/lib/ts/recipe/openid/recipeImplementation.ts deleted file mode 100644 index a5529e32a..000000000 --- a/lib/ts/recipe/openid/recipeImplementation.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { RecipeInterface, TypeNormalisedInput } from "./types"; -import { RecipeInterface as JWTRecipeInterface, JsonWebKey } from "../jwt/types"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { GET_JWKS_API } from "../jwt/constants"; - -export default function getRecipeInterface( - config: TypeNormalisedInput, - jwtRecipeImplementation: JWTRecipeInterface -): RecipeInterface { - return { - getOpenIdDiscoveryConfiguration: async function (): Promise<{ - status: "OK"; - issuer: string; - jwks_uri: string; - }> { - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - let jwks_uri = - config.issuerDomain.getAsStringDangerous() + - config.issuerPath.appendPath(new NormalisedURLPath(GET_JWKS_API)).getAsStringDangerous(); - return { - status: "OK", - issuer, - jwks_uri, - }; - }, - createJWT: async function ({ - payload, - validitySeconds, - userContext, - }: { - payload?: any; - validitySeconds?: number; - userContext: any; - }): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - > { - payload = payload === undefined || payload === null ? {} : payload; - - let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); - return await jwtRecipeImplementation.createJWT({ - payload: { - iss: issuer, - ...payload, - }, - validitySeconds, - userContext, - }); - }, - getJWKS: async function (input): Promise<{ status: "OK"; keys: JsonWebKey[] }> { - return await jwtRecipeImplementation.getJWKS(input); - }, - }; -} diff --git a/lib/ts/recipe/openid/types.ts b/lib/ts/recipe/openid/types.ts deleted file mode 100644 index 70168efa5..000000000 --- a/lib/ts/recipe/openid/types.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import OverrideableBuilder from "supertokens-js-override"; -import { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLDomain from "../../normalisedURLDomain"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; -import { GeneralErrorResponse } from "../../types"; - -export type TypeInput = { - issuer?: string; - jwtValiditySeconds?: number; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; -}; - -export type TypeNormalisedInput = { - issuerDomain: NormalisedURLDomain; - issuerPath: NormalisedURLPath; - jwtValiditySeconds?: number; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - req: BaseRequest; - res: BaseResponse; -}; - -export type APIInterface = { - getOpenIdDiscoveryConfigurationGET: - | undefined - | ((input: { - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - issuer: string; - jwks_uri: string; - } - | GeneralErrorResponse - >); -}; - -export type RecipeInterface = { - getOpenIdDiscoveryConfiguration(input: { - userContext: any; - }): Promise<{ - status: "OK"; - issuer: string; - jwks_uri: string; - }>; - createJWT(input: { - payload?: any; - validitySeconds?: number; - userContext: any; - }): Promise< - | { - status: "OK"; - jwt: string; - } - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; - } - >; - - getJWKS(input: { - userContext: any; - }): Promise<{ - status: "OK"; - keys: JsonWebKey[]; - }>; -}; diff --git a/lib/ts/recipe/openid/utils.ts b/lib/ts/recipe/openid/utils.ts deleted file mode 100644 index 72414d9c9..000000000 --- a/lib/ts/recipe/openid/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import NormalisedURLDomain from "../../normalisedURLDomain"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { NormalisedAppinfo } from "../../types"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; - -export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config?: TypeInput): TypeNormalisedInput { - let issuerDomain = appInfo.apiDomain; - let issuerPath = appInfo.apiBasePath; - - if (config !== undefined) { - if (config.issuer !== undefined) { - issuerDomain = new NormalisedURLDomain(config.issuer); - issuerPath = new NormalisedURLPath(config.issuer); - } - - if (!issuerPath.equals(appInfo.apiBasePath)) { - throw new Error("The path of the issuer URL must be equal to the apiBasePath. The default value is /auth"); - } - } - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - return { - issuerDomain, - issuerPath, - jwtValiditySeconds: config?.jwtValiditySeconds, - override, - }; -} diff --git a/lib/ts/recipe/passwordless/api/consumeCode.ts b/lib/ts/recipe/passwordless/api/consumeCode.ts deleted file mode 100644 index ea441c3fd..000000000 --- a/lib/ts/recipe/passwordless/api/consumeCode.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from ".."; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function consumeCode(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.consumeCodePOST === undefined) { - return false; - } - - const body = await options.req.getJSONBody(); - const preAuthSessionId = body.preAuthSessionId; - const linkCode = body.linkCode; - const deviceId = body.deviceId; - const userInputCode = body.userInputCode; - - if (preAuthSessionId === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide preAuthSessionId", - }); - } - - if (deviceId !== undefined || userInputCode !== undefined) { - if (linkCode !== undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", - }); - } - if (deviceId === undefined || userInputCode === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide both deviceId and userInputCode", - }); - } - } else if (linkCode === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", - }); - } - - const userContext = makeDefaultUserContextFromAPI(options.req); - let result = await apiImplementation.consumeCodePOST( - deviceId !== undefined - ? { - deviceId, - userInputCode, - preAuthSessionId, - options, - userContext, - } - : { - linkCode, - options, - preAuthSessionId, - userContext, - } - ); - - if (result.status === "OK") { - delete (result as any).session; - } - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/passwordless/api/createCode.ts b/lib/ts/recipe/passwordless/api/createCode.ts deleted file mode 100644 index 1fe266e56..000000000 --- a/lib/ts/recipe/passwordless/api/createCode.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from ".."; -import parsePhoneNumber from "libphonenumber-js/max"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function createCode(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.createCodePOST === undefined) { - return false; - } - - const body = await options.req.getJSONBody(); - let email: string | undefined = body.email; - let phoneNumber: string | undefined = body.phoneNumber; - - if ((email !== undefined && phoneNumber !== undefined) || (email === undefined && phoneNumber === undefined)) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide exactly one of email or phoneNumber", - }); - } - - if (email === undefined && options.config.contactMethod === "EMAIL") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: 'Please provide an email since you have set the contactMethod to "EMAIL"', - }); - } - - if (phoneNumber === undefined && options.config.contactMethod === "PHONE") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: 'Please provide a phoneNumber since you have set the contactMethod to "PHONE"', - }); - } - - // normalise and validate format of input - if ( - email !== undefined && - (options.config.contactMethod === "EMAIL" || options.config.contactMethod === "EMAIL_OR_PHONE") - ) { - email = email.trim(); - const validateError = await options.config.validateEmailAddress(email); - if (validateError !== undefined) { - send200Response(options.res, { - status: "GENERAL_ERROR", - message: validateError, - }); - return true; - } - } - - if ( - phoneNumber !== undefined && - (options.config.contactMethod === "PHONE" || options.config.contactMethod === "EMAIL_OR_PHONE") - ) { - const validateError = await options.config.validatePhoneNumber(phoneNumber); - if (validateError !== undefined) { - send200Response(options.res, { - status: "GENERAL_ERROR", - message: validateError, - }); - return true; - } - const parsedPhoneNumber = parsePhoneNumber(phoneNumber); - if (parsedPhoneNumber === undefined) { - // this can come here if the user has provided their own impl of validatePhoneNumber and - // the phone number is valid according to their impl, but not according to the libphonenumber-js lib. - phoneNumber = phoneNumber.trim(); - } else { - phoneNumber = parsedPhoneNumber.format("E.164"); - } - } - - let result = await apiImplementation.createCodePOST( - email !== undefined - ? { email, options, userContext: makeDefaultUserContextFromAPI(options.req) } - : { phoneNumber: phoneNumber!, options, userContext: makeDefaultUserContextFromAPI(options.req) } - ); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/passwordless/api/emailExists.ts b/lib/ts/recipe/passwordless/api/emailExists.ts deleted file mode 100644 index ab79c4bf6..000000000 --- a/lib/ts/recipe/passwordless/api/emailExists.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function emailExists(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.emailExistsGET === undefined) { - return false; - } - - let email = options.req.getKeyValueFromQuery("email"); - - if (email === undefined || typeof email !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the email as a GET param", - }); - } - - let result = await apiImplementation.emailExistsGET({ - email, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/passwordless/api/implementation.ts b/lib/ts/recipe/passwordless/api/implementation.ts deleted file mode 100644 index d708a07d3..000000000 --- a/lib/ts/recipe/passwordless/api/implementation.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { APIInterface } from "../"; -import { logDebugMessage } from "../../../logger"; -import EmailVerification from "../../emailverification/recipe"; -import Session from "../../session"; - -export default function getAPIImplementation(): APIInterface { - return { - consumeCodePOST: async function (input) { - let response = await input.options.recipeImplementation.consumeCode( - "deviceId" in input - ? { - preAuthSessionId: input.preAuthSessionId, - deviceId: input.deviceId, - userInputCode: input.userInputCode, - userContext: input.userContext, - } - : { - preAuthSessionId: input.preAuthSessionId, - linkCode: input.linkCode, - userContext: input.userContext, - } - ); - - if (response.status !== "OK") { - return response; - } - - let user = response.user; - - if (user.email !== undefined) { - const emailVerificationInstance = EmailVerification.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: user.id, - email: user.email, - userContext: input.userContext, - } - ); - - if (tokenResponse.status === "OK") { - await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - userContext: input.userContext, - }); - } - } - } - - const session = await Session.createNewSession( - input.options.req, - input.options.res, - user.id, - {}, - {}, - input.userContext - ); - - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - session, - }; - }, - createCodePOST: async function (input) { - let response = await input.options.recipeImplementation.createCode( - "email" in input - ? { - userContext: input.userContext, - email: input.email, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : await input.options.config.getCustomUserInputCode(input.userContext), - } - : { - userContext: input.userContext, - phoneNumber: input.phoneNumber, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : await input.options.config.getCustomUserInputCode(input.userContext), - } - ); - - // now we send the email / text message. - let magicLink: string | undefined = undefined; - let userInputCode: string | undefined = undefined; - const flowType = input.options.config.flowType; - if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - magicLink = - input.options.appInfo.websiteDomain.getAsStringDangerous() + - input.options.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - input.options.recipeId + - "&preAuthSessionId=" + - response.preAuthSessionId + - "#" + - response.linkCode; - } - if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - userInputCode = response.userInputCode; - } - - // we don't do something special for serverless env here - // cause we want to wait for service's reply since it can show - // a UI error message for if sending an SMS / email failed or not. - if ( - input.options.config.contactMethod === "PHONE" || - (input.options.config.contactMethod === "EMAIL_OR_PHONE" && "phoneNumber" in input) - ) { - logDebugMessage(`Sending passwordless login SMS to ${(input as any).phoneNumber}`); - await input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ - type: "PASSWORDLESS_LOGIN", - codeLifetime: response.codeLifetime, - phoneNumber: (input as any).phoneNumber!, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } else { - logDebugMessage(`Sending passwordless login email to ${(input as any).email}`); - await input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORDLESS_LOGIN", - email: (input as any).email!, - codeLifetime: response.codeLifetime, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } - - return { - status: "OK", - deviceId: response.deviceId, - flowType: input.options.config.flowType, - preAuthSessionId: response.preAuthSessionId, - }; - }, - emailExistsGET: async function (input) { - let response = await input.options.recipeImplementation.getUserByEmail({ - userContext: input.userContext, - email: input.email, - }); - - return { - exists: response !== undefined, - status: "OK", - }; - }, - phoneNumberExistsGET: async function (input) { - let response = await input.options.recipeImplementation.getUserByPhoneNumber({ - userContext: input.userContext, - phoneNumber: input.phoneNumber, - }); - - return { - exists: response !== undefined, - status: "OK", - }; - }, - resendCodePOST: async function (input) { - let deviceInfo = await input.options.recipeImplementation.listCodesByDeviceId({ - userContext: input.userContext, - deviceId: input.deviceId, - }); - - if (deviceInfo === undefined) { - return { - status: "RESTART_FLOW_ERROR", - }; - } - - if ( - (input.options.config.contactMethod === "PHONE" && deviceInfo.phoneNumber === undefined) || - (input.options.config.contactMethod === "EMAIL" && deviceInfo.email === undefined) - ) { - return { - status: "RESTART_FLOW_ERROR", - }; - } - - let numberOfTriesToCreateNewCode = 0; - while (true) { - numberOfTriesToCreateNewCode++; - let response = await input.options.recipeImplementation.createNewCodeForDevice({ - userContext: input.userContext, - deviceId: input.deviceId, - userInputCode: - input.options.config.getCustomUserInputCode === undefined - ? undefined - : await input.options.config.getCustomUserInputCode(input.userContext), - }); - - if (response.status === "USER_INPUT_CODE_ALREADY_USED_ERROR") { - if (numberOfTriesToCreateNewCode >= 3) { - // we retry 3 times. - return { - status: "GENERAL_ERROR", - message: "Failed to generate a one time code. Please try again", - }; - } - continue; - } - - if (response.status === "OK") { - let magicLink: string | undefined = undefined; - let userInputCode: string | undefined = undefined; - const flowType = input.options.config.flowType; - if (flowType === "MAGIC_LINK" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - magicLink = - input.options.appInfo.websiteDomain.getAsStringDangerous() + - input.options.appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - input.options.recipeId + - "&preAuthSessionId=" + - response.preAuthSessionId + - "#" + - response.linkCode; - } - if (flowType === "USER_INPUT_CODE" || flowType === "USER_INPUT_CODE_AND_MAGIC_LINK") { - userInputCode = response.userInputCode; - } - - // we don't do something special for serverless env here - // cause we want to wait for service's reply since it can show - // a UI error message for if sending an SMS / email failed or not. - if ( - input.options.config.contactMethod === "PHONE" || - (input.options.config.contactMethod === "EMAIL_OR_PHONE" && - deviceInfo.phoneNumber !== undefined) - ) { - logDebugMessage(`Sending passwordless login SMS to ${(input as any).phoneNumber}`); - await input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ - type: "PASSWORDLESS_LOGIN", - codeLifetime: response.codeLifetime, - phoneNumber: deviceInfo.phoneNumber!, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } else { - logDebugMessage(`Sending passwordless login email to ${(input as any).email}`); - await input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORDLESS_LOGIN", - email: deviceInfo.email!, - codeLifetime: response.codeLifetime, - preAuthSessionId: response.preAuthSessionId, - urlWithLinkCode: magicLink, - userInputCode, - userContext: input.userContext, - }); - } - } - - return { - status: response.status, - }; - } - }, - }; -} diff --git a/lib/ts/recipe/passwordless/api/phoneNumberExists.ts b/lib/ts/recipe/passwordless/api/phoneNumberExists.ts deleted file mode 100644 index 9dd9ae8fc..000000000 --- a/lib/ts/recipe/passwordless/api/phoneNumberExists.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from ".."; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function phoneNumberExists( - apiImplementation: APIInterface, - options: APIOptions -): Promise { - if (apiImplementation.phoneNumberExistsGET === undefined) { - return false; - } - - let phoneNumber = options.req.getKeyValueFromQuery("phoneNumber"); - - if (phoneNumber === undefined || typeof phoneNumber !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the phoneNumber as a GET param", - }); - } - - let result = await apiImplementation.phoneNumberExistsGET({ - phoneNumber, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/passwordless/api/resendCode.ts b/lib/ts/recipe/passwordless/api/resendCode.ts deleted file mode 100644 index 21ecac4ce..000000000 --- a/lib/ts/recipe/passwordless/api/resendCode.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from ".."; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function resendCode(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.resendCodePOST === undefined) { - return false; - } - - const body = await options.req.getJSONBody(); - const preAuthSessionId = body.preAuthSessionId; - const deviceId = body.deviceId; - - if (preAuthSessionId === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide preAuthSessionId", - }); - } - - if (deviceId === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide a deviceId", - }); - } - - let result = await apiImplementation.resendCodePOST({ - deviceId, - preAuthSessionId, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/passwordless/constants.ts b/lib/ts/recipe/passwordless/constants.ts deleted file mode 100644 index 11a8a49dc..000000000 --- a/lib/ts/recipe/passwordless/constants.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const CREATE_CODE_API = "/signinup/code"; - -export const RESEND_CODE_API = "/signinup/code/resend"; - -export const CONSUME_CODE_API = "/signinup/code/consume"; - -export const DOES_EMAIL_EXIST_API = "/signup/email/exists"; - -export const DOES_PHONE_NUMBER_EXIST_API = "/signup/phonenumber/exists"; diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts deleted file mode 100644 index 909f38900..000000000 --- a/lib/ts/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import axios, { AxiosError } from "axios"; -import { NormalisedAppinfo } from "../../../../../types"; -import { logDebugMessage } from "../../../../../logger"; - -function defaultCreateAndSendCustomEmail(appInfo: NormalisedAppinfo) { - return async ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - }, - _: any - ): Promise => { - if (process.env.TEST_MODE === "testing") { - return; - } - try { - await axios({ - method: "POST", - url: "https://api.supertokens.io/0/st/auth/passwordless/login", - data: { - email: input.email, - appName: appInfo.appName, - codeLifetime: input.codeLifetime, - urlWithLinkCode: input.urlWithLinkCode, - userInputCode: input.userInputCode, - }, - headers: { - "api-version": 0, - }, - }); - logDebugMessage(`Email sent to ${input.email}`); - } catch (error) { - logDebugMessage("Error sending passwordless login email"); - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - if (err.response) { - logDebugMessage(`Error status: ${err.response.status}`); - logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logDebugMessage(`Error: ${err.message}`); - } - } else { - logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logDebugMessage("Logging the input below:"); - logDebugMessage( - JSON.stringify( - { - email: input.email, - appName: appInfo.appName, - codeLifetime: input.codeLifetime, - urlWithLinkCode: input.urlWithLinkCode, - userInputCode: input.userInputCode, - }, - null, - 2 - ) - ); - /** - * if the error is thrown from API, the response object - * will be of type `{err: string}` - */ - if (axios.isAxiosError(error) && error.response !== undefined) { - if (error.response.data.err !== undefined) { - throw Error(error.response.data.err); - } - } - throw error; - } - }; -} - -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private createAndSendCustomEmail: ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - - constructor( - appInfo: NormalisedAppinfo, - createAndSendCustomEmail?: ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise - ) { - this.createAndSendCustomEmail = - createAndSendCustomEmail === undefined - ? defaultCreateAndSendCustomEmail(appInfo) - : createAndSendCustomEmail; - } - - sendEmail = async (input: TypePasswordlessEmailDeliveryInput & { userContext: any }) => { - await this.createAndSendCustomEmail( - { - email: input.email, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - preAuthSessionId: input.preAuthSessionId, - codeLifetime: input.codeLifetime, - }, - input.userContext - ); - }; -} diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/index.ts b/lib/ts/recipe/passwordless/emaildelivery/services/index.ts deleted file mode 100644 index 5d393e47d..000000000 --- a/lib/ts/recipe/passwordless/emaildelivery/services/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import SMTP from "./smtp"; -export let SMTPService = SMTP; diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/passwordless/emaildelivery/services/smtp/index.ts deleted file mode 100644 index 273b535f7..000000000 --- a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { createTransport } from "nodemailer"; -import OverrideableBuilder from "supertokens-js-override"; -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -import { getServiceImplementation } from "./serviceImplementation"; - -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - - constructor(config: TypeInput) { - const transporter = createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } - - sendEmail = async (input: TypePasswordlessEmailDeliveryInput & { userContext: any }) => { - let content = await this.serviceImpl.getContent(input); - await this.serviceImpl.sendRawEmail({ - ...content, - userContext: input.userContext, - }); - }; -} diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.ts b/lib/ts/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.ts deleted file mode 100644 index 10760d733..000000000 --- a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.ts +++ /dev/null @@ -1,2886 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/emaildelivery/services/smtp"; -import Supertokens from "../../../../../supertokens"; -import { humaniseMilliseconds } from "../../../../../utils"; -export default function getPasswordlessLoginEmailContent(input: TypePasswordlessEmailDeliveryInput): GetContentResult { - let supertokens = Supertokens.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getPasswordlessLoginEmailHTML( - appName, - input.email, - input.codeLifetime, - input.urlWithLinkCode, - input.userInputCode - ); - return { - body, - toEmail: input.email, - subject: "Login to your account", - isHtml: true, - }; -} - -function getPasswordlessLoginOTPBody(appName: string, email: string, codeLifetime: string, userInputCode: string) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
-

- Login to ${appName}

-
- - - - - - -
- - -
-
- -

- Enter the below OTP in your login screen. Note - that the OTP expires in ${codeLifetime}.

- -
-
- ${userInputCode}
- -
-
-
-
- - - - - - -
- - -

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} - -function getPasswordlessLoginURLLinkBody( - appName: string, - email: string, - codeLifetime: string, - urlWithLinkCode: string -) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
-

- Login to ${appName}

-
- - - - - - -
- - -
-
- -

- Please click the button below to sign in / up. - Note that the link expires in ${codeLifetime}. -

- -
- Login -
-
-
-

- Alternatively, you can directly paste this link - in your browser
- ${urlWithLinkCode} -

-
-
- - - - -
- - - - - - -
- - -

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} - -function getPasswordlessLoginOTPAndURLLinkBody( - appName: string, - email: string, - codeLifetime: string, - urlWithLinkCode: string, - userInputCode: string -) { - return ` - - - - - - - - *|MC:SUBJECT|* - - - - - - - - - -
- - - - -
- - - - - - - - - - - -
- - - - - -
- -
- - - - - -
- - - - - - -
-

- Login to ${appName}

-
- - - - - - -
-
-
-

- Enter the below OTP in your login screen. Note - that the OTP expires in ${codeLifetime}.

-
- -
-
- ${userInputCode}
-
-
-
- - - - - - -
- - - - - - - - - - -
- - or -
- - - -
- - - - - - -
- - -
-
- -

- Please click the button below to sign in / up. - Note that the link expires in ${codeLifetime}.

- -
- Login -
-
- -
-

- Alternatively, you can directly paste this link - in your browser
- ${urlWithLinkCode} -

-
-
- - -
- - - - - - -
-

- This email is meant for ${email} -

-
-
- -
- - - - - -
- -
- -
-
- - - - `; -} - -export function getPasswordlessLoginEmailHTML( - appName: string, - email: string, - codeLifetime: number, - urlWithLinkCode?: string, - userInputCode?: string -): string { - if (urlWithLinkCode !== undefined && userInputCode !== undefined) { - return getPasswordlessLoginOTPAndURLLinkBody( - appName, - email, - humaniseMilliseconds(codeLifetime), - urlWithLinkCode, - userInputCode - ); - } - if (userInputCode !== undefined) { - return getPasswordlessLoginOTPBody(appName, email, humaniseMilliseconds(codeLifetime), userInputCode); - } - if (urlWithLinkCode !== undefined) { - return getPasswordlessLoginURLLinkBody(appName, email, humaniseMilliseconds(codeLifetime), urlWithLinkCode); - } - throw Error("this should never be thrown"); -} diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts b/lib/ts/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts deleted file mode 100644 index 067838bba..000000000 --- a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypePasswordlessEmailDeliveryInput } from "../../../types"; -import { Transporter } from "nodemailer"; -import { - ServiceInterface, - TypeInputSendRawEmail, - GetContentResult, -} from "../../../../../ingredients/emaildelivery/services/smtp"; -import getPasswordlessLoginEmailContent from "./passwordlessLogin"; - -export function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface { - return { - sendRawEmail: async function (input: TypeInputSendRawEmail) { - if (input.isHtml) { - await transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - } else { - await transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - text: input.body, - }); - } - }, - getContent: async function ( - input: TypePasswordlessEmailDeliveryInput & { userContext: any } - ): Promise { - return getPasswordlessLoginEmailContent(input); - }, - }; -} diff --git a/lib/ts/recipe/passwordless/error.ts b/lib/ts/recipe/passwordless/error.ts deleted file mode 100644 index f6c4147ef..000000000 --- a/lib/ts/recipe/passwordless/error.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; - -export default class SessionError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }) { - super({ - ...options, - }); - this.fromRecipe = "passwordless"; - } -} diff --git a/lib/ts/recipe/passwordless/index.ts b/lib/ts/recipe/passwordless/index.ts deleted file mode 100644 index d1a3e7ef5..000000000 --- a/lib/ts/recipe/passwordless/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import { - RecipeInterface, - User, - APIOptions, - APIInterface, - TypePasswordlessEmailDeliveryInput, - TypePasswordlessSmsDeliveryInput, -} from "./types"; - -export default class Wrapper { - static init = Recipe.init; - - static Error = SuperTokensError; - - static createCode( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { userInputCode?: string; userContext?: any } - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ - userContext: {}, - ...input, - }); - } - - static createNewCodeForDevice(input: { deviceId: string; userInputCode?: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ - userContext: {}, - ...input, - }); - } - - static consumeCode( - input: - | { - preAuthSessionId: string; - userInputCode: string; - deviceId: string; - userContext?: any; - } - | { - preAuthSessionId: string; - linkCode: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ userContext: {}, ...input }); - } - - static getUserById(input: { userId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userContext: {}, ...input }); - } - - static getUserByEmail(input: { email: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ userContext: {}, ...input }); - } - - static getUserByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByPhoneNumber({ userContext: {}, ...input }); - } - - static updateUser(input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext?: any; - }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateUser({ userContext: {}, ...input }); - } - - static revokeAllCodes( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ userContext: {}, ...input }); - } - - static revokeCode(input: { codeId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ userContext: {}, ...input }); - } - - static listCodesByEmail(input: { email: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ userContext: {}, ...input }); - } - - static listCodesByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ - userContext: {}, - ...input, - }); - } - - static listCodesByDeviceId(input: { deviceId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ userContext: {}, ...input }); - } - - static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ - userContext: {}, - ...input, - }); - } - - static createMagicLink( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().createMagicLink({ userContext: {}, ...input }); - } - - static signInUp( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().signInUp({ userContext: {}, ...input }); - } - - static async sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: any }) { - return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, - ...input, - }); - } - - static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { - return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ - userContext: {}, - ...input, - }); - } -} - -export let init = Wrapper.init; - -export let Error = Wrapper.Error; - -export let createCode = Wrapper.createCode; - -export let consumeCode = Wrapper.consumeCode; - -export let getUserByEmail = Wrapper.getUserByEmail; - -export let getUserById = Wrapper.getUserById; - -export let getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; - -export let listCodesByDeviceId = Wrapper.listCodesByDeviceId; - -export let listCodesByEmail = Wrapper.listCodesByEmail; - -export let listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber; - -export let listCodesByPreAuthSessionId = Wrapper.listCodesByPreAuthSessionId; - -export let createNewCodeForDevice = Wrapper.createNewCodeForDevice; - -export let updateUser = Wrapper.updateUser; - -export let revokeAllCodes = Wrapper.revokeAllCodes; - -export let revokeCode = Wrapper.revokeCode; - -export let createMagicLink = Wrapper.createMagicLink; - -export let signInUp = Wrapper.signInUp; - -export type { RecipeInterface, User, APIOptions, APIInterface }; - -export let sendEmail = Wrapper.sendEmail; - -export let sendSms = Wrapper.sendSms; diff --git a/lib/ts/recipe/passwordless/recipe.ts b/lib/ts/recipe/passwordless/recipe.ts deleted file mode 100644 index 392b02581..000000000 --- a/lib/ts/recipe/passwordless/recipe.ts +++ /dev/null @@ -1,330 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import RecipeModule from "../../recipeModule"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import STError from "./error"; -import { validateAndNormaliseUserInput } from "./utils"; -import NormalisedURLPath from "../../normalisedURLPath"; -import EmailVerificationRecipe from "../emailverification/recipe"; -import RecipeImplementation from "./recipeImplementation"; -import APIImplementation from "./api/implementation"; -import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import consumeCodeAPI from "./api/consumeCode"; -import createCodeAPI from "./api/createCode"; -import emailExistsAPI from "./api/emailExists"; -import phoneNumberExistsAPI from "./api/phoneNumberExists"; -import resendCodeAPI from "./api/resendCode"; -import { - CONSUME_CODE_API, - CREATE_CODE_API, - DOES_EMAIL_EXIST_API, - DOES_PHONE_NUMBER_EXIST_API, - RESEND_CODE_API, -} from "./constants"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput } from "./types"; -import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; -import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "passwordless"; - - config: TypeNormalisedInput; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - emailDelivery: EmailDeliveryIngredient; - - smsDelivery: SmsDeliveryIngredient; - - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - smsDelivery: SmsDeliveryIngredient | undefined; - } - ) { - super(recipeId, appInfo); - this.isInServerlessEnv = isInServerlessEnv; - this.config = validateAndNormaliseUserInput(this, appInfo, config); - - { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new EmailDeliveryIngredient(this.config.getEmailDeliveryConfig()) - : ingredients.emailDelivery; - - this.smsDelivery = - ingredients.smsDelivery === undefined - ? new SmsDeliveryIngredient(this.config.getSmsDeliveryConfig()) - : ingredients.smsDelivery; - - PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = EmailVerificationRecipe.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static init(config: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { - emailDelivery: undefined, - smsDelivery: undefined, - }); - return Recipe.instance; - } else { - throw new Error("Passwordless recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - // abstract instance functions below............... - - getAPIsHandled = (): APIHandled[] => { - return [ - { - id: CONSUME_CODE_API, - disabled: this.apiImpl.consumeCodePOST === undefined, - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(CONSUME_CODE_API), - }, - { - id: CREATE_CODE_API, - disabled: this.apiImpl.createCodePOST === undefined, - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(CREATE_CODE_API), - }, - { - id: DOES_EMAIL_EXIST_API, - disabled: this.apiImpl.emailExistsGET === undefined, - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(DOES_EMAIL_EXIST_API), - }, - { - id: DOES_PHONE_NUMBER_EXIST_API, - disabled: this.apiImpl.phoneNumberExistsGET === undefined, - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(DOES_PHONE_NUMBER_EXIST_API), - }, - { - id: RESEND_CODE_API, - disabled: this.apiImpl.resendCodePOST === undefined, - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(RESEND_CODE_API), - }, - ]; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - _: NormalisedURLPath, - __: HTTPMethod - ): Promise => { - const options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - emailDelivery: this.emailDelivery, - smsDelivery: this.smsDelivery, - appInfo: this.getAppInfo(), - }; - if (id === CONSUME_CODE_API) { - return await consumeCodeAPI(this.apiImpl, options); - } else if (id === CREATE_CODE_API) { - return await createCodeAPI(this.apiImpl, options); - } else if (id === DOES_EMAIL_EXIST_API) { - return await emailExistsAPI(this.apiImpl, options); - } else if (id === DOES_PHONE_NUMBER_EXIST_API) { - return await phoneNumberExistsAPI(this.apiImpl, options); - } else { - return await resendCodeAPI(this.apiImpl, options); - } - }; - - handleError = async (err: STError, _: BaseRequest, __: BaseResponse): Promise => { - throw err; - }; - - getAllCORSHeaders = (): string[] => { - return []; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - - // helper functions below... - - createMagicLink = async ( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ): Promise => { - let userInputCode = - this.config.getCustomUserInputCode !== undefined - ? await this.config.getCustomUserInputCode(input.userContext) - : undefined; - - const codeInfo = await this.recipeInterfaceImpl.createCode( - "email" in input - ? { - email: input.email, - userInputCode, - userContext: input.userContext, - } - : { - phoneNumber: input.phoneNumber, - userInputCode, - userContext: input.userContext, - } - ); - - const appInfo = this.getAppInfo(); - - let magicLink = - appInfo.websiteDomain.getAsStringDangerous() + - appInfo.websiteBasePath.getAsStringDangerous() + - "/verify" + - "?rid=" + - this.getRecipeId() + - "&preAuthSessionId=" + - codeInfo.preAuthSessionId + - "#" + - codeInfo.linkCode; - - return magicLink; - }; - - signInUp = async ( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) => { - let codeInfo = await this.recipeInterfaceImpl.createCode( - "email" in input - ? { - email: input.email, - userContext: input.userContext, - } - : { - phoneNumber: input.phoneNumber, - userContext: input.userContext, - } - ); - - let consumeCodeResponse = await this.recipeInterfaceImpl.consumeCode( - this.config.flowType === "MAGIC_LINK" - ? { - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - userContext: input.userContext, - } - : { - preAuthSessionId: codeInfo.preAuthSessionId, - deviceId: codeInfo.deviceId, - userInputCode: codeInfo.userInputCode, - userContext: input.userContext, - } - ); - - if (consumeCodeResponse.status === "OK") { - return { - status: "OK", - createdNewUser: consumeCodeResponse.createdNewUser, - user: consumeCodeResponse.user, - }; - } else { - throw new Error("Failed to create user. Please retry"); - } - }; - - // helper functions... - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - let userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - if (userInfo.email !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "EMAIL_DOES_NOT_EXIST_ERROR", - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }; -} diff --git a/lib/ts/recipe/passwordless/recipeImplementation.ts b/lib/ts/recipe/passwordless/recipeImplementation.ts deleted file mode 100644 index 363ed6670..000000000 --- a/lib/ts/recipe/passwordless/recipeImplementation.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { RecipeInterface } from "./types"; -import { Querier } from "../../querier"; -import NormalisedURLPath from "../../normalisedURLPath"; - -export default function getRecipeInterface(querier: Querier): RecipeInterface { - function copyAndRemoveUserContext(input: any): any { - let result = { - ...input, - }; - delete result.userContext; - return result; - } - - return { - consumeCode: async function (input) { - let response = await querier.sendPostRequest( - new NormalisedURLPath("/recipe/signinup/code/consume"), - copyAndRemoveUserContext(input) - ); - return response; - }, - createCode: async function (input) { - let response = await querier.sendPostRequest( - new NormalisedURLPath("/recipe/signinup/code"), - copyAndRemoveUserContext(input) - ); - return response; - }, - createNewCodeForDevice: async function (input) { - let response = await querier.sendPostRequest( - new NormalisedURLPath("/recipe/signinup/code"), - copyAndRemoveUserContext(input) - ); - return response; - }, - getUserByEmail: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/user"), - copyAndRemoveUserContext(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }, - getUserById: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/user"), - copyAndRemoveUserContext(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }, - getUserByPhoneNumber: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/user"), - copyAndRemoveUserContext(input) - ); - if (response.status === "OK") { - return response.user; - } - return undefined; - }, - listCodesByDeviceId: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices.length === 1 ? response.devices[0] : undefined; - }, - listCodesByEmail: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices; - }, - listCodesByPhoneNumber: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices; - }, - listCodesByPreAuthSessionId: async function (input) { - let response = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/signinup/codes"), - copyAndRemoveUserContext(input) - ); - return response.devices.length === 1 ? response.devices[0] : undefined; - }, - revokeAllCodes: async function (input) { - await querier.sendPostRequest( - new NormalisedURLPath("/recipe/signinup/codes/remove"), - copyAndRemoveUserContext(input) - ); - return { - status: "OK", - }; - }, - revokeCode: async function (input) { - await querier.sendPostRequest( - new NormalisedURLPath("/recipe/signinup/code/remove"), - copyAndRemoveUserContext(input) - ); - return { status: "OK" }; - }, - updateUser: async function (input) { - let response = await querier.sendPutRequest( - new NormalisedURLPath("/recipe/user"), - copyAndRemoveUserContext(input) - ); - return response; - }, - }; -} diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts deleted file mode 100644 index a140fb03c..000000000 --- a/lib/ts/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { NormalisedAppinfo } from "../../../../../types"; -import axios, { AxiosError } from "axios"; -import { SUPERTOKENS_SMS_SERVICE_URL } from "../../../../../ingredients/smsdelivery/services/supertokens"; -import Supertokens from "../../../../../supertokens"; -import { logDebugMessage } from "../../../../../logger"; - -function defaultCreateAndSendCustomSms(_: NormalisedAppinfo) { - return async ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - }, - _: any - ): Promise => { - let supertokens = Supertokens.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - try { - await axios({ - method: "post", - url: SUPERTOKENS_SMS_SERVICE_URL, - data: { - smsInput: { - appName, - type: "PASSWORDLESS_LOGIN", - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - }, - }, - headers: { - "api-version": "0", - }, - }); - logDebugMessage(`Passwordless login SMS sent to ${input.phoneNumber}`); - return; - } catch (error) { - logDebugMessage("Error sending passwordless login SMS"); - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - if (err.response) { - logDebugMessage(`Error status: ${err.response.status}`); - logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logDebugMessage(`Error: ${err.message}`); - } - if (err.response) { - if (err.response.status !== 429) { - /** - * if the error is thrown from API, the response object - * will be of type `{err: string}` - */ - if (err.response.data.err !== undefined) { - throw Error(err.response.data.err); - } else { - throw err; - } - } - } else { - throw err; - } - } else { - logDebugMessage(`Error: ${JSON.stringify(error)}`); - throw error; - } - } - console.log( - "Free daily SMS quota reached. If you want to use SuperTokens to send SMS, please sign up on supertokens.com to get your SMS API key, else you can also define your own method by overriding the service. For now, we are logging it below:" - ); - /** - * if we do console.log(`SMS content: ${input}`); - * Output would be: - * SMS content: [object Object] - */ - /** - * JSON.stringify takes 3 inputs - * - value: usually an object or array, to be converted - * - replacer: An array of strings and numbers that acts - * as an approved list for selecting the object - * properties that will be stringified - * - space: Adds indentation, white space, and line break characters - * to the return-value JSON text to make it easier to read - * - * console.log(JSON.stringify({"a": 1, "b": 2})) - * Output: - * {"a":1,"b":2} - * - * console.log(JSON.stringify({"a": 1, "b": 2}, null, 2)) - * Output: - * { - * "a": 1, - * "b": 2 - * } - */ - console.log(`\nSMS content: ${JSON.stringify(input, null, 2)}`); - }; -} - -export default class BackwardCompatibilityService implements SmsDeliveryInterface { - private createAndSendCustomSms: ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - - constructor( - appInfo: NormalisedAppinfo, - createAndSendCustomSms?: ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise - ) { - this.createAndSendCustomSms = - createAndSendCustomSms === undefined ? defaultCreateAndSendCustomSms(appInfo) : createAndSendCustomSms; - } - - sendSms = async (input: TypePasswordlessSmsDeliveryInput & { userContext: any }) => { - await this.createAndSendCustomSms( - { - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - preAuthSessionId: input.preAuthSessionId, - codeLifetime: input.codeLifetime, - }, - input.userContext - ); - }; -} diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/index.ts b/lib/ts/recipe/passwordless/smsdelivery/services/index.ts deleted file mode 100644 index 882d9afeb..000000000 --- a/lib/ts/recipe/passwordless/smsdelivery/services/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Twilio from "./twilio"; -import Supertokens from "./supertokens"; - -export let TwilioService = Twilio; -export let SupertokensService = Supertokens; diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/supertokens/index.ts b/lib/ts/recipe/passwordless/smsdelivery/services/supertokens/index.ts deleted file mode 100644 index 8a9317342..000000000 --- a/lib/ts/recipe/passwordless/smsdelivery/services/supertokens/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { SUPERTOKENS_SMS_SERVICE_URL } from "../../../../../ingredients/smsdelivery/services/supertokens"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import axios, { AxiosError } from "axios"; -import Supertokens from "../../../../../supertokens"; -import { logDebugMessage } from "../../../../../logger"; - -export default class SupertokensService implements SmsDeliveryInterface { - private apiKey: string; - - constructor(apiKey: string) { - this.apiKey = apiKey; - } - - sendSms = async (input: TypePasswordlessSmsDeliveryInput) => { - let supertokens = Supertokens.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - try { - await axios({ - method: "post", - url: SUPERTOKENS_SMS_SERVICE_URL, - data: { - apiKey: this.apiKey, - smsInput: { - type: input.type, - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - appName, - }, - }, - headers: { - "api-version": "0", - }, - }); - } catch (error) { - logDebugMessage("Error sending SMS"); - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - if (err.response) { - logDebugMessage(`Error status: ${err.response.status}`); - logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`); - } else { - logDebugMessage(`Error: ${err.message}`); - } - } else { - logDebugMessage(`Error: ${JSON.stringify(error)}`); - } - logDebugMessage("Logging the input below:"); - logDebugMessage( - JSON.stringify( - { - type: input.type, - phoneNumber: input.phoneNumber, - userInputCode: input.userInputCode, - urlWithLinkCode: input.urlWithLinkCode, - codeLifetime: input.codeLifetime, - appName, - }, - null, - 2 - ) - ); - throw error; - } - }; -} diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/index.ts b/lib/ts/recipe/passwordless/smsdelivery/services/twilio/index.ts deleted file mode 100644 index 6885830de..000000000 --- a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { - ServiceInterface, - TypeInput, - normaliseUserInputConfig, -} from "../../../../../ingredients/smsdelivery/services/twilio"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import Twilio from "twilio"; -import OverrideableBuilder from "supertokens-js-override"; -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import { getServiceImplementation } from "./serviceImplementation"; - -export default class TwilioService implements SmsDeliveryInterface { - serviceImpl: ServiceInterface; - private config: TypeInput; - - constructor(config: TypeInput) { - this.config = normaliseUserInputConfig(config); - const twilioClient = Twilio( - config.twilioSettings.accountSid, - config.twilioSettings.authToken, - config.twilioSettings.opts - ); - let builder = new OverrideableBuilder(getServiceImplementation(twilioClient)); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - } - - sendSms = async (input: TypePasswordlessSmsDeliveryInput & { userContext: any }) => { - let content = await this.serviceImpl.getContent(input); - if ("from" in this.config.twilioSettings) { - await this.serviceImpl.sendRawSms({ - ...content, - userContext: input.userContext, - from: this.config.twilioSettings.from, - }); - } else { - await this.serviceImpl.sendRawSms({ - ...content, - userContext: input.userContext, - messagingServiceSid: this.config.twilioSettings.messagingServiceSid, - }); - } - }; -} diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.ts b/lib/ts/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.ts deleted file mode 100644 index cb0bb91c3..000000000 --- a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import { GetContentResult } from "../../../../../ingredients/smsdelivery/services/twilio"; -import { humaniseMilliseconds } from "../../../../../utils"; -import Supertokens from "../../../../../supertokens"; - -export default function getPasswordlessLoginSmsContent(input: TypePasswordlessSmsDeliveryInput): GetContentResult { - let supertokens = Supertokens.getInstanceOrThrowError(); - let appName = supertokens.appInfo.appName; - let body = getPasswordlessLoginSmsBody(appName, input.codeLifetime, input.urlWithLinkCode, input.userInputCode); - return { - body, - toPhoneNumber: input.phoneNumber, - }; -} - -function getPasswordlessLoginSmsBody( - appName: string, - codeLifetime: number, - urlWithLinkCode: string | undefined, - userInputCode: string | undefined -) { - let message = ""; - if (urlWithLinkCode !== undefined && userInputCode !== undefined) { - message += `OTP to login is ${userInputCode} for ${appName}\n\nOR click ${urlWithLinkCode} to login.\n\n`; - } else if (urlWithLinkCode !== undefined) { - message += `Click ${urlWithLinkCode} to login to ${appName}\n\n`; - } else { - message += `OTP to login is ${userInputCode} for ${appName}\n\n`; - } - const humanisedCodeLifetime = humaniseMilliseconds(codeLifetime); - message += `This is valid for ${humanisedCodeLifetime}.`; - return message; -} diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts b/lib/ts/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts deleted file mode 100644 index 03931bbcd..000000000 --- a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypePasswordlessSmsDeliveryInput } from "../../../types"; -import Twilio from "twilio/lib/rest/Twilio"; -import { - ServiceInterface, - TypeInputSendRawSms, - GetContentResult, -} from "../../../../../ingredients/smsdelivery/services/twilio"; -import getPasswordlessLoginSmsContent from "./passwordlessLogin"; - -export function getServiceImplementation(twilioClient: Twilio): ServiceInterface { - return { - sendRawSms: async function (input: TypeInputSendRawSms) { - if ("from" in input) { - await twilioClient.messages.create({ - to: input.toPhoneNumber, - body: input.body, - from: input.from, - }); - } else { - await twilioClient.messages.create({ - to: input.toPhoneNumber, - body: input.body, - messagingServiceSid: input.messagingServiceSid, - }); - } - }, - getContent: async function ( - input: TypePasswordlessSmsDeliveryInput & { userContext: any } - ): Promise { - return getPasswordlessLoginSmsContent(input); - }, - }; -} diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts deleted file mode 100644 index 5b6f3889a..000000000 --- a/lib/ts/recipe/passwordless/types.ts +++ /dev/null @@ -1,414 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { - TypeInput as SmsDeliveryTypeInput, - TypeInputWithService as SmsDeliveryTypeInputWithService, -} from "../../ingredients/smsdelivery/types"; -import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; - -// As per https://github.com/supertokens/supertokens-core/issues/325 - -export type User = { - id: string; - email?: string; - phoneNumber?: string; - timeJoined: number; -}; - -export type TypeInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } -) & { - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - - emailDelivery?: EmailDeliveryTypeInput; - smsDelivery?: SmsDeliveryTypeInput; - - // Override this to override how user input codes are generated - // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (userContext: any) => Promise | string; - - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber: (phoneNumber: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress: (email: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress: (email: string) => Promise | string | undefined; - - validatePhoneNumber: (phoneNumber: string) => Promise | string | undefined; - } -) & { - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - - // Override this to override how user input codes are generated - // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (userContext: any) => Promise | string; - - getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService; - getEmailDeliveryConfig: () => EmailDeliveryTypeInputWithService; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder: OverrideableBuilder) => APIInterface; - }; -}; - -export type RecipeInterface = { - createCode: ( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { userInputCode?: string; userContext: any } - ) => Promise<{ - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - }>; - createNewCodeForDevice: (input: { - deviceId: string; - userInputCode?: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - } - | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" } - >; - consumeCode: ( - input: - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - userContext: any; - } - | { - linkCode: string; - preAuthSessionId: string; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | { status: "RESTART_FLOW_ERROR" } - >; - - getUserById: (input: { userId: string; userContext: any }) => Promise; - getUserByEmail: (input: { email: string; userContext: any }) => Promise; - getUserByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - - updateUser: (input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; - - revokeAllCodes: ( - input: - | { - email: string; - userContext: any; - } - | { - phoneNumber: string; - userContext: any; - } - ) => Promise<{ - status: "OK"; - }>; - - revokeCode: (input: { - codeId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - }>; - - listCodesByEmail: (input: { email: string; userContext: any }) => Promise; - - listCodesByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - - listCodesByDeviceId: (input: { deviceId: string; userContext: any }) => Promise; - - listCodesByPreAuthSessionId: (input: { - preAuthSessionId: string; - userContext: any; - }) => Promise; -}; - -export type DeviceType = { - preAuthSessionId: string; - - failedCodeInputAttemptCount: number; - - email?: string; - phoneNumber?: string; - - codes: { - codeId: string; - timeCreated: string; - codeLifetime: number; - }[]; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - appInfo: NormalisedAppinfo; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; - emailDelivery: EmailDeliveryIngredient; - smsDelivery: SmsDeliveryIngredient; -}; - -export type APIInterface = { - createCodePOST?: ( - input: ({ email: string } | { phoneNumber: string }) & { - options: APIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - deviceId: string; - preAuthSessionId: string; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - } - | GeneralErrorResponse - >; - - resendCodePOST?: ( - input: { deviceId: string; preAuthSessionId: string } & { - options: APIOptions; - userContext: any; - } - ) => Promise; - - consumeCodePOST?: ( - input: ( - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - } - | { - linkCode: string; - preAuthSessionId: string; - } - ) & { - options: APIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | GeneralErrorResponse - | { status: "RESTART_FLOW_ERROR" } - >; - - emailExistsGET?: (input: { - email: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >; - - phoneNumberExistsGET?: (input: { - phoneNumber: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >; -}; - -export type TypePasswordlessEmailDeliveryInput = { - type: "PASSWORDLESS_LOGIN"; - email: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; -}; - -export type TypePasswordlessSmsDeliveryInput = { - type: "PASSWORDLESS_LOGIN"; - phoneNumber: string; - userInputCode?: string; - urlWithLinkCode?: string; - codeLifetime: number; - preAuthSessionId: string; -}; diff --git a/lib/ts/recipe/passwordless/utils.ts b/lib/ts/recipe/passwordless/utils.ts deleted file mode 100644 index 343eed274..000000000 --- a/lib/ts/recipe/passwordless/utils.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo } from "../../types"; -import parsePhoneNumber from "libphonenumber-js/max"; -import BackwardCompatibilityEmailService from "./emaildelivery/services/backwardCompatibility"; -import BackwardCompatibilitySmsService from "./smsdelivery/services/backwardCompatibility"; - -export function validateAndNormaliseUserInput( - _: Recipe, - appInfo: NormalisedAppinfo, - config: TypeInput -): TypeNormalisedInput { - if ( - config.contactMethod !== "PHONE" && - config.contactMethod !== "EMAIL" && - config.contactMethod !== "EMAIL_OR_PHONE" - ) { - throw new Error('Please pass one of "PHONE", "EMAIL" or "EMAIL_OR_PHONE" as the contactMethod'); - } - - if (config.flowType === undefined) { - throw new Error("Please pass flowType argument in the config"); - } - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config.override, - }; - - function getEmailDeliveryConfig() { - let emailService = config.emailDelivery?.service; - - let createAndSendCustomEmail = config.contactMethod === "PHONE" ? undefined : config.createAndSendCustomEmail; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation - */ - if (emailService === undefined) { - emailService = new BackwardCompatibilityEmailService(appInfo, createAndSendCustomEmail); - } - let emailDelivery = { - ...config.emailDelivery, - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }; - return emailDelivery; - } - - function getSmsDeliveryConfig() { - let smsService = config.smsDelivery?.service; - - let createAndSendCustomTextMessage = - config.contactMethod === "EMAIL" ? undefined : config.createAndSendCustomTextMessage; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomTextMessage config. If the user - * has not passed even that config, we use the default - * createAndSendCustomTextMessage implementation - */ - if (smsService === undefined) { - smsService = new BackwardCompatibilitySmsService(appInfo, createAndSendCustomTextMessage); - } - let smsDelivery = { - ...config.smsDelivery, - /** - * if we do - * let smsDelivery = { - * service: smsService, - * ...config.smsDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: smsService, - }; - return smsDelivery; - } - if (config.contactMethod === "EMAIL") { - return { - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - flowType: config.flowType, - contactMethod: "EMAIL", - validateEmailAddress: - config.validateEmailAddress === undefined ? defaultValidateEmail : config.validateEmailAddress, - getCustomUserInputCode: config.getCustomUserInputCode, - }; - } else if (config.contactMethod === "PHONE") { - return { - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - flowType: config.flowType, - contactMethod: "PHONE", - validatePhoneNumber: - config.validatePhoneNumber === undefined ? defaultValidatePhoneNumber : config.validatePhoneNumber, - getCustomUserInputCode: config.getCustomUserInputCode, - }; - } else { - return { - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - flowType: config.flowType, - contactMethod: "EMAIL_OR_PHONE", - validateEmailAddress: - config.validateEmailAddress === undefined ? defaultValidateEmail : config.validateEmailAddress, - validatePhoneNumber: - config.validatePhoneNumber === undefined ? defaultValidatePhoneNumber : config.validatePhoneNumber, - getCustomUserInputCode: config.getCustomUserInputCode, - }; - } -} - -export function defaultValidatePhoneNumber(value: string): Promise | string | undefined { - if (typeof value !== "string") { - return "Development bug: Please make sure the phoneNumber field is a string"; - } - - let parsedPhoneNumber = parsePhoneNumber(value); - if (parsedPhoneNumber === undefined || !parsedPhoneNumber.isValid()) { - return "Phone number is invalid"; - } - - // we can even use Twilio's phone number validity lookup service: https://www.twilio.com/docs/glossary/what-e164. - - return undefined; -} - -export function defaultValidateEmail(value: string): Promise | string | undefined { - // We check if the email syntax is correct - // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 - // Regex from https://stackoverflow.com/a/46181/3867175 - - if (typeof value !== "string") { - return "Development bug: Please make sure the email field is a string"; - } - - if ( - value.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) === null - ) { - return "Email is invalid"; - } - - return undefined; -} diff --git a/lib/ts/recipe/session/accessToken.ts b/lib/ts/recipe/session/accessToken.ts deleted file mode 100644 index 9172b80d0..000000000 --- a/lib/ts/recipe/session/accessToken.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "./error"; -import { ParsedJWTInfo, verifyJWT } from "./jwt"; - -export async function getInfoFromAccessToken( - jwtInfo: ParsedJWTInfo, - jwtSigningPublicKey: string, - doAntiCsrfCheck: boolean -): Promise<{ - sessionHandle: string; - userId: string; - refreshTokenHash1: string; - parentRefreshTokenHash1: string | undefined; - userData: any; - antiCsrfToken: string | undefined; - expiryTime: number; - timeCreated: number; -}> { - try { - verifyJWT(jwtInfo, jwtSigningPublicKey); - const payload = jwtInfo.payload; - - // This should be called before this function, but the check is very quick, so we can also do them here - validateAccessTokenStructure(payload); - - // We can mark these as defined (the ! after the calls), since validateAccessTokenPayload checks this - let sessionHandle = sanitizeStringInput(payload.sessionHandle)!; - let userId = sanitizeStringInput(payload.userId)!; - let refreshTokenHash1 = sanitizeStringInput(payload.refreshTokenHash1)!; - let parentRefreshTokenHash1 = sanitizeStringInput(payload.parentRefreshTokenHash1); - let userData = payload.userData; - let antiCsrfToken = sanitizeStringInput(payload.antiCsrfToken); - let expiryTime = sanitizeNumberInput(payload.expiryTime)!; - let timeCreated = sanitizeNumberInput(payload.timeCreated)!; - - if (antiCsrfToken === undefined && doAntiCsrfCheck) { - throw Error("Access token does not contain the anti-csrf token."); - } - - if (expiryTime < Date.now()) { - throw Error("Access token expired"); - } - return { - sessionHandle, - userId, - refreshTokenHash1, - parentRefreshTokenHash1, - userData, - antiCsrfToken, - expiryTime, - timeCreated, - }; - } catch (err) { - throw new STError({ - message: "Failed to verify access token", - type: STError.TRY_REFRESH_TOKEN, - }); - } -} - -export function validateAccessTokenStructure(payload: any) { - if ( - typeof payload.sessionHandle !== "string" || - typeof payload.userId !== "string" || - typeof payload.refreshTokenHash1 !== "string" || - payload.userData === undefined || - typeof payload.expiryTime !== "number" || - typeof payload.timeCreated !== "number" - ) { - // it would come here if we change the structure of the JWT. - throw Error("Access token does not contain all the information. Maybe the structure has changed?"); - } -} - -function sanitizeStringInput(field: any): string | undefined { - if (field === "") { - return ""; - } - if (typeof field !== "string") { - return undefined; - } - try { - let result = field.trim(); - return result; - } catch (err) {} - return undefined; -} - -export function sanitizeNumberInput(field: any): number | undefined { - if (typeof field === "number") { - return field; - } - return undefined; -} diff --git a/lib/ts/recipe/session/api/implementation.ts b/lib/ts/recipe/session/api/implementation.ts deleted file mode 100644 index 1d54fcb7a..000000000 --- a/lib/ts/recipe/session/api/implementation.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { APIInterface, APIOptions, VerifySessionOptions } from "../"; -import { normaliseHttpMethod } from "../../../utils"; -import NormalisedURLPath from "../../../normalisedURLPath"; -import { SessionContainerInterface } from "../types"; -import { GeneralErrorResponse } from "../../../types"; -import { getRequiredClaimValidators } from "../utils"; - -export default function getAPIInterface(): APIInterface { - return { - refreshPOST: async function ({ - options, - userContext, - }: { - options: APIOptions; - userContext: any; - }): Promise { - return await options.recipeImplementation.refreshSession({ - req: options.req, - res: options.res, - userContext, - }); - }, - - verifySession: async function ({ - verifySessionOptions, - options, - userContext, - }: { - verifySessionOptions: VerifySessionOptions | undefined; - options: APIOptions; - userContext: any; - }): Promise { - let method = normaliseHttpMethod(options.req.getMethod()); - if (method === "options" || method === "trace") { - return undefined; - } - - let incomingPath = new NormalisedURLPath(options.req.getOriginalURL()); - - let refreshTokenPath = options.config.refreshTokenPath; - - if (incomingPath.equals(refreshTokenPath) && method === "post") { - return options.recipeImplementation.refreshSession({ - req: options.req, - res: options.res, - userContext, - }); - } else { - const session = await options.recipeImplementation.getSession({ - req: options.req, - res: options.res, - options: verifySessionOptions, - userContext, - }); - if (session !== undefined) { - const claimValidators = await getRequiredClaimValidators( - session, - verifySessionOptions?.overrideGlobalClaimValidators, - userContext - ); - - await session.assertClaims(claimValidators, userContext); - } - - return session; - } - }, - - signOutPOST: async function ({ - session, - userContext, - }: { - options: APIOptions; - session: SessionContainerInterface | undefined; - userContext: any; - }): Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - > { - if (session !== undefined) { - await session.revokeSession(userContext); - } - - return { - status: "OK", - }; - }, - }; -} diff --git a/lib/ts/recipe/session/api/refresh.ts b/lib/ts/recipe/session/api/refresh.ts deleted file mode 100644 index ec785ce20..000000000 --- a/lib/ts/recipe/session/api/refresh.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function handleRefreshAPI(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.refreshPOST === undefined) { - return false; - } - - await apiImplementation.refreshPOST({ options, userContext: makeDefaultUserContextFromAPI(options.req) }); - send200Response(options.res, {}); - return true; -} diff --git a/lib/ts/recipe/session/api/signout.ts b/lib/ts/recipe/session/api/signout.ts deleted file mode 100644 index 12702f3f9..000000000 --- a/lib/ts/recipe/session/api/signout.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function signOutAPI(apiImplementation: APIInterface, options: APIOptions): Promise { - // Logic as per https://github.com/supertokens/supertokens-node/issues/34#issuecomment-717958537 - - if (apiImplementation.signOutPOST === undefined) { - return false; - } - - let defaultUserContext = makeDefaultUserContextFromAPI(options.req); - - const session = await options.recipeImplementation.getSession({ - req: options.req, - res: options.res, - options: { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, - userContext: defaultUserContext, - }); - - let result = await apiImplementation.signOutPOST({ - options, - session, - userContext: defaultUserContext, - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/session/claimBaseClasses/booleanClaim.ts b/lib/ts/recipe/session/claimBaseClasses/booleanClaim.ts deleted file mode 100644 index e80a7ca57..000000000 --- a/lib/ts/recipe/session/claimBaseClasses/booleanClaim.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SessionClaim, SessionClaimValidator } from "../types"; -import { PrimitiveClaim } from "./primitiveClaim"; - -export class BooleanClaim extends PrimitiveClaim { - constructor(conf: { - key: string; - fetchValue: SessionClaim["fetchValue"]; - defaultMaxAgeInSeconds?: number; - }) { - super(conf); - - this.validators = { - ...this.validators, - isTrue: (maxAge?: number, id?: string): SessionClaimValidator => this.validators.hasValue(true, maxAge, id), - isFalse: (maxAge?: number, id?: string): SessionClaimValidator => - this.validators.hasValue(false, maxAge, id), - }; - } - - validators!: PrimitiveClaim["validators"] & { - isTrue: (maxAge?: number, id?: string) => SessionClaimValidator; - isFalse: (maxAge?: number, id?: string) => SessionClaimValidator; - }; -} diff --git a/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts b/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts deleted file mode 100644 index b35df4e60..000000000 --- a/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { JSONPrimitive } from "../../../types"; -import { SessionClaim, SessionClaimValidator } from "../types"; - -export class PrimitiveArrayClaim extends SessionClaim { - public readonly fetchValue: (userId: string, userContext: any) => Promise | T[] | undefined; - public readonly defaultMaxAgeInSeconds: number | undefined; - - constructor(config: { key: string; fetchValue: SessionClaim["fetchValue"]; defaultMaxAgeInSeconds?: number }) { - super(config.key); - this.fetchValue = config.fetchValue; - this.defaultMaxAgeInSeconds = config.defaultMaxAgeInSeconds; - } - - addToPayload_internal(payload: any, value: T[], _userContext: any): any { - return { - ...payload, - [this.key]: { - v: value, - t: Date.now(), - }, - }; - } - removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any { - const res = { - ...payload, - [this.key]: null, - }; - - return res; - } - - removeFromPayload(payload: any, _userContext?: any): any { - const res = { - ...payload, - }; - delete res[this.key]; - - return res; - } - - getValueFromPayload(payload: any, _userContext?: any): T[] | undefined { - return payload[this.key]?.v; - } - - getLastRefetchTime(payload: any, _userContext?: any): number | undefined { - return payload[this.key]?.t; - } - - validators = { - includes: ( - val: T, - maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, - id?: string - ): SessionClaimValidator => { - return { - claim: this, - id: id ?? this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: async (payload, ctx) => { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { message: "value does not exist", expectedToInclude: val, actualValue: claimVal }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (!claimVal.includes(val)) { - return { - isValid: false, - reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, - }; - } - return { isValid: true }; - }, - }; - }, - excludes: ( - val: T, - maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, - id?: string - ): SessionClaimValidator => { - return { - claim: this, - id: id ?? this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: async (payload, ctx) => { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - if (claimVal.includes(val)) { - return { - isValid: false, - reason: { message: "wrong value", expectedToNotInclude: val, actualValue: claimVal }, - }; - } - return { isValid: true }; - }, - }; - }, - includesAll: ( - val: T[], - maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, - id?: string - ): SessionClaimValidator => { - return { - claim: this, - id: id ?? this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: async (payload, ctx) => { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { message: "value does not exist", expectedToInclude: val, actualValue: claimVal }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - const claimSet = new Set(claimVal); - const isValid = val.every((v) => claimSet.has(v)); - return isValid - ? { isValid } - : { - isValid, - reason: { message: "wrong value", expectedToInclude: val, actualValue: claimVal }, - }; - }, - }; - }, - excludesAll: ( - val: T[], - maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, - id?: string - ): SessionClaimValidator => { - return { - claim: this, - id: id ?? this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - // We know payload[this.id] is defined since the value is not undefined in this branch - (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: async (payload, ctx) => { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { - message: "value does not exist", - expectedToNotInclude: val, - actualValue: claimVal, - }, - }; - } - - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - const claimSet = new Set(claimVal); - const isValid = val.every((v) => !claimSet.has(v)); - return isValid - ? { isValid: isValid } - : { - isValid, - reason: { message: "wrong value", expectedToNotInclude: val, actualValue: claimVal }, - }; - }, - }; - }, - }; -} diff --git a/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts b/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts deleted file mode 100644 index d94a8686e..000000000 --- a/lib/ts/recipe/session/claimBaseClasses/primitiveClaim.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { JSONPrimitive } from "../../../types"; -import { SessionClaim, SessionClaimValidator } from "../types"; - -export class PrimitiveClaim extends SessionClaim { - public readonly fetchValue: (userId: string, userContext: any) => Promise | T | undefined; - public readonly defaultMaxAgeInSeconds: number | undefined; - - constructor(config: { key: string; fetchValue: SessionClaim["fetchValue"]; defaultMaxAgeInSeconds?: number }) { - super(config.key); - this.fetchValue = config.fetchValue; - this.defaultMaxAgeInSeconds = config.defaultMaxAgeInSeconds; - } - - addToPayload_internal(payload: any, value: T, _userContext: any): any { - return { - ...payload, - [this.key]: { - v: value, - t: Date.now(), - }, - }; - } - removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any { - const res = { - ...payload, - [this.key]: null, - }; - - return res; - } - - removeFromPayload(payload: any, _userContext?: any): any { - const res = { - ...payload, - }; - delete res[this.key]; - - return res; - } - - getValueFromPayload(payload: any, _userContext?: any): T | undefined { - return payload[this.key]?.v; - } - - getLastRefetchTime(payload: any, _userContext?: any): number | undefined { - return payload[this.key]?.t; - } - - validators = { - hasValue: ( - val: T, - maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, - id?: string - ): SessionClaimValidator => { - return { - claim: this, - id: id ?? this.key, - shouldRefetch: (payload, ctx) => - this.getValueFromPayload(payload, ctx) === undefined || - (maxAgeInSeconds !== undefined && // We know payload[this.id] is defined since the value is not undefined in this branch - payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), - validate: async (payload, ctx) => { - const claimVal = this.getValueFromPayload(payload, ctx); - if (claimVal === undefined) { - return { - isValid: false, - reason: { message: "value does not exist", expectedValue: val, actualValue: claimVal }, - }; - } - const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000; - if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { - return { - isValid: false, - reason: { - message: "expired", - ageInSeconds, - maxAgeInSeconds, - }, - }; - } - - if (claimVal !== val) { - return { - isValid: false, - reason: { message: "wrong value", expectedValue: val, actualValue: claimVal }, - }; - } - return { isValid: true }; - }, - }; - }, - }; -} diff --git a/lib/ts/recipe/session/claims.ts b/lib/ts/recipe/session/claims.ts deleted file mode 100644 index 03561b33f..000000000 --- a/lib/ts/recipe/session/claims.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { SessionClaim } from "./types"; -export { PrimitiveClaim } from "./claimBaseClasses/primitiveClaim"; -export { PrimitiveArrayClaim } from "./claimBaseClasses/primitiveArrayClaim"; -export { BooleanClaim } from "./claimBaseClasses/booleanClaim"; diff --git a/lib/ts/recipe/session/constants.ts b/lib/ts/recipe/session/constants.ts deleted file mode 100644 index a8a5fb90e..000000000 --- a/lib/ts/recipe/session/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TokenTransferMethod } from "./types"; - -export const REFRESH_API_PATH = "/session/refresh"; -export const SIGNOUT_API_PATH = "/signout"; - -export const availableTokenTransferMethods: TokenTransferMethod[] = ["cookie", "header"]; diff --git a/lib/ts/recipe/session/cookieAndHeaders.ts b/lib/ts/recipe/session/cookieAndHeaders.ts deleted file mode 100644 index 8bb2f91ec..000000000 --- a/lib/ts/recipe/session/cookieAndHeaders.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { HEADER_RID } from "../../constants"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { availableTokenTransferMethods } from "./constants"; -import { TokenTransferMethod, TokenType, TypeNormalisedInput } from "./types"; - -const authorizationHeaderKey = "authorization"; -const accessTokenCookieKey = "sAccessToken"; -const accessTokenHeaderKey = "st-access-token"; -const refreshTokenCookieKey = "sRefreshToken"; -const refreshTokenHeaderKey = "st-refresh-token"; - -const antiCsrfHeaderKey = "anti-csrf"; - -const frontTokenHeaderKey = "front-token"; - -const authModeHeaderKey = "st-auth-mode"; - -export function clearSessionFromAllTokenTransferMethods(config: TypeNormalisedInput, res: BaseResponse) { - // We are clearing the session in all transfermethods to be sure to override cookies in case they have been already added to the response. - // This is done to handle the following use-case: - // If the app overrides signInPOST to check the ban status of the user after the original implementation and throwing an UNAUTHORISED error - // In this case: the SDK has attached cookies to the response, but none was sent with the request - // We can't know which to clear since we can't reliably query or remove the set-cookie header added to the response (causes issues in some frameworks, i.e.: hapi) - // The safe solution in this case is to overwrite all the response cookies/headers with an empty value, which is what we are doing here - for (const transferMethod of availableTokenTransferMethods) { - clearSession(config, res, transferMethod); - } -} - -export function clearSession(config: TypeNormalisedInput, res: BaseResponse, transferMethod: TokenTransferMethod) { - // If we can be specific about which transferMethod we want to clear, there is no reason to clear the other ones - const tokenTypes: TokenType[] = ["access", "refresh"]; - for (const token of tokenTypes) { - setToken(config, res, token, "", 0, transferMethod); - } - - res.removeHeader(antiCsrfHeaderKey); - // This can be added multiple times in some cases, but that should be OK - res.setHeader(frontTokenHeaderKey, "remove", false); - res.setHeader("Access-Control-Expose-Headers", frontTokenHeaderKey, true); -} - -export function getAntiCsrfTokenFromHeaders(req: BaseRequest): string | undefined { - return req.getHeaderValue(antiCsrfHeaderKey); -} - -export function setAntiCsrfTokenInHeaders(res: BaseResponse, antiCsrfToken: string) { - res.setHeader(antiCsrfHeaderKey, antiCsrfToken, false); - res.setHeader("Access-Control-Expose-Headers", antiCsrfHeaderKey, true); -} - -export function setFrontTokenInHeaders(res: BaseResponse, userId: string, atExpiry: number, accessTokenPayload: any) { - const tokenInfo = { - uid: userId, - ate: atExpiry, - up: accessTokenPayload, - }; - res.setHeader(frontTokenHeaderKey, Buffer.from(JSON.stringify(tokenInfo)).toString("base64"), false); - res.setHeader("Access-Control-Expose-Headers", frontTokenHeaderKey, true); -} - -export function getCORSAllowedHeaders(): string[] { - return [antiCsrfHeaderKey, HEADER_RID, authorizationHeaderKey, authModeHeaderKey]; -} - -function getCookieNameFromTokenType(tokenType: TokenType) { - switch (tokenType) { - case "access": - return accessTokenCookieKey; - case "refresh": - return refreshTokenCookieKey; - default: - throw new Error("Unknown token type, should never happen."); - } -} - -function getResponseHeaderNameForTokenType(tokenType: TokenType) { - switch (tokenType) { - case "access": - return accessTokenHeaderKey; - case "refresh": - return refreshTokenHeaderKey; - default: - throw new Error("Unknown token type, should never happen."); - } -} - -export function getToken(req: BaseRequest, tokenType: TokenType, transferMethod: TokenTransferMethod) { - if (transferMethod === "cookie") { - return req.getCookieValue(getCookieNameFromTokenType(tokenType)); - } else if (transferMethod === "header") { - const value = req.getHeaderValue(authorizationHeaderKey); - if (value === undefined || !value.startsWith("Bearer ")) { - return undefined; - } - - return value.replace(/^Bearer /, "").trim(); - } else { - throw new Error("Should never happen: Unknown transferMethod: " + transferMethod); - } -} - -export function setToken( - config: TypeNormalisedInput, - res: BaseResponse, - tokenType: TokenType, - value: string, - expires: number, - transferMethod: TokenTransferMethod -) { - if (transferMethod === "cookie") { - setCookie( - config, - res, - getCookieNameFromTokenType(tokenType), - value, - expires, - tokenType === "refresh" ? "refreshTokenPath" : "accessTokenPath" - ); - } else if (transferMethod === "header") { - setHeader(res, getResponseHeaderNameForTokenType(tokenType), value); - } -} - -export function setHeader(res: BaseResponse, name: string, value: string) { - res.setHeader(name, value, false); - res.setHeader("Access-Control-Expose-Headers", name, true); -} - -/** - * - * @param res - * @param name - * @param value - * @param domain - * @param secure - * @param httpOnly - * @param expires - * @param path - */ -export function setCookie( - config: TypeNormalisedInput, - res: BaseResponse, - name: string, - value: string, - expires: number, - pathType: "refreshTokenPath" | "accessTokenPath" -) { - let domain = config.cookieDomain; - let secure = config.cookieSecure; - let sameSite = config.cookieSameSite; - let path = ""; - if (pathType === "refreshTokenPath") { - path = config.refreshTokenPath.getAsStringDangerous(); - } else if (pathType === "accessTokenPath") { - path = - config.accessTokenPath.getAsStringDangerous() === "" ? "/" : config.accessTokenPath.getAsStringDangerous(); - } - let httpOnly = true; - - return res.setCookie(name, value, domain, secure, httpOnly, expires, path, sameSite); -} - -export function getAuthModeFromHeader(req: BaseRequest): string | undefined { - return req.getHeaderValue(authModeHeaderKey)?.toLowerCase(); -} diff --git a/lib/ts/recipe/session/error.ts b/lib/ts/recipe/session/error.ts deleted file mode 100644 index 546364aa4..000000000 --- a/lib/ts/recipe/session/error.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; -import { ClaimValidationError } from "./types"; - -export default class SessionError extends STError { - static UNAUTHORISED: "UNAUTHORISED" = "UNAUTHORISED"; - static TRY_REFRESH_TOKEN: "TRY_REFRESH_TOKEN" = "TRY_REFRESH_TOKEN"; - static TOKEN_THEFT_DETECTED: "TOKEN_THEFT_DETECTED" = "TOKEN_THEFT_DETECTED"; - static INVALID_CLAIMS: "INVALID_CLAIMS" = "INVALID_CLAIMS"; - - constructor( - options: - | { - message: string; - type: "UNAUTHORISED"; - payload?: { - clearTokens: boolean; - }; - } - | { - message: string; - type: "TRY_REFRESH_TOKEN"; - } - | { - message: string; - type: "TOKEN_THEFT_DETECTED"; - payload: { - userId: string; - sessionHandle: string; - }; - } - | { - message: string; - type: "INVALID_CLAIMS"; - payload: ClaimValidationError[]; - } - ) { - super( - options.type === "UNAUTHORISED" && options.payload === undefined - ? { - ...options, - payload: { - clearTokens: true, - }, - } - : { ...options } - ); - this.fromRecipe = "session"; - } -} diff --git a/lib/ts/recipe/session/framework/awsLambda.ts b/lib/ts/recipe/session/framework/awsLambda.ts deleted file mode 100644 index b10d01a64..000000000 --- a/lib/ts/recipe/session/framework/awsLambda.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import type { Handler, Context, Callback } from "aws-lambda"; -import { AWSRequest, AWSResponse } from "../../../framework/awsLambda/framework"; -import type { SessionEvent, SessionEventV2 } from "../../../framework/awsLambda/framework"; -import SuperTokens from "../../../supertokens"; -import Session from "../recipe"; -import { VerifySessionOptions } from ".."; - -export function verifySession(handler: Handler, verifySessionOptions?: VerifySessionOptions): Handler { - return async (event: SessionEvent | SessionEventV2, context: Context, callback: Callback) => { - let supertokens = SuperTokens.getInstanceOrThrowError(); - let request = new AWSRequest(event); - let response = new AWSResponse(event); - try { - let sessionRecipe = Session.getInstanceOrThrowError(); - event.session = await sessionRecipe.verifySession(verifySessionOptions, request, response); - let handlerResult = await handler(event, context, callback); - return response.sendResponse(handlerResult); - } catch (err) { - await supertokens.errorHandler(err, request, response); - if (response.responseSet) { - return response.sendResponse({}); - } - throw err; - } - }; -} diff --git a/lib/ts/recipe/session/framework/express.ts b/lib/ts/recipe/session/framework/express.ts deleted file mode 100644 index 29c047880..000000000 --- a/lib/ts/recipe/session/framework/express.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Session from "../recipe"; -import type { VerifySessionOptions } from ".."; -import type { SessionRequest } from "../../../framework/express/framework"; -import { ExpressRequest, ExpressResponse } from "../../../framework/express/framework"; -import type { NextFunction, Response } from "express"; -import SuperTokens from "../../../supertokens"; - -export function verifySession(options?: VerifySessionOptions) { - return async (req: SessionRequest, res: Response, next: NextFunction) => { - const request = new ExpressRequest(req); - const response = new ExpressResponse(res); - try { - const sessionRecipe = Session.getInstanceOrThrowError(); - req.session = await sessionRecipe.verifySession(options, request, response); - next(); - } catch (err) { - try { - const supertokens = SuperTokens.getInstanceOrThrowError(); - await supertokens.errorHandler(err, request, response); - } catch { - next(err); - } - } - }; -} diff --git a/lib/ts/recipe/session/framework/fastify.ts b/lib/ts/recipe/session/framework/fastify.ts deleted file mode 100644 index 1b13afef9..000000000 --- a/lib/ts/recipe/session/framework/fastify.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Session from "../recipe"; -import { VerifySessionOptions } from ".."; -import { FastifyRequest, FastifyResponse, SessionRequest } from "../../../framework/fastify/framework"; -import { FastifyReply } from "fastify"; -import SuperTokens from "../../../supertokens"; - -export function verifySession(options?: VerifySessionOptions) { - return async (req: SessionRequest, res: FastifyReply) => { - let sessionRecipe = Session.getInstanceOrThrowError(); - let request = new FastifyRequest(req); - let response = new FastifyResponse(res); - try { - req.session = await sessionRecipe.verifySession(options, request, response); - } catch (err) { - const supertokens = SuperTokens.getInstanceOrThrowError(); - await supertokens.errorHandler(err, request, response); - throw err; - } - }; -} diff --git a/lib/ts/recipe/session/framework/hapi.ts b/lib/ts/recipe/session/framework/hapi.ts deleted file mode 100644 index b463b793c..000000000 --- a/lib/ts/recipe/session/framework/hapi.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Session from "../recipe"; -import { VerifySessionOptions } from ".."; -import { ResponseToolkit } from "@hapi/hapi"; -import { ExtendedResponseToolkit, HapiRequest, HapiResponse, SessionRequest } from "../../../framework/hapi/framework"; - -export function verifySession(options?: VerifySessionOptions) { - return async (req: SessionRequest, h: ResponseToolkit) => { - let sessionRecipe = Session.getInstanceOrThrowError(); - let request = new HapiRequest(req); - let response = new HapiResponse(h as ExtendedResponseToolkit); - req.session = await sessionRecipe.verifySession(options, request, response); - return h.continue; - }; -} diff --git a/lib/ts/recipe/session/framework/index.ts b/lib/ts/recipe/session/framework/index.ts deleted file mode 100644 index c984f7879..000000000 --- a/lib/ts/recipe/session/framework/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import * as expressFramework from "./express"; -import * as fastifyFramework from "./fastify"; -import * as hapiFramework from "./hapi"; -import * as loopbackFramework from "./loopback"; -import * as koaFramework from "./koa"; -import * as awsLambdaFramework from "./awsLambda"; - -export default { - express: expressFramework, - fastify: fastifyFramework, - hapi: hapiFramework, - loopback: loopbackFramework, - koa: koaFramework, - awsLambda: awsLambdaFramework, -}; - -export let express = expressFramework; -export let fastify = fastifyFramework; -export let hapi = hapiFramework; -export let loopback = loopbackFramework; -export let koa = koaFramework; -export let awsLambda = awsLambdaFramework; diff --git a/lib/ts/recipe/session/framework/koa.ts b/lib/ts/recipe/session/framework/koa.ts deleted file mode 100644 index da50945ab..000000000 --- a/lib/ts/recipe/session/framework/koa.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Session from "../recipe"; -import type { VerifySessionOptions } from ".."; -import type { Next } from "koa"; -import { KoaRequest, KoaResponse } from "../../../framework/koa/framework"; -import type { SessionContext } from "../../../framework/koa/framework"; - -export function verifySession(options?: VerifySessionOptions) { - return async (ctx: SessionContext, next: Next) => { - let sessionRecipe = Session.getInstanceOrThrowError(); - let request = new KoaRequest(ctx); - let response = new KoaResponse(ctx); - ctx.session = await sessionRecipe.verifySession(options, request, response); - await next(); - }; -} diff --git a/lib/ts/recipe/session/framework/loopback.ts b/lib/ts/recipe/session/framework/loopback.ts deleted file mode 100644 index 7bf2cfc6f..000000000 --- a/lib/ts/recipe/session/framework/loopback.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Session from "../recipe"; -import { VerifySessionOptions } from ".."; -import { InterceptorOrKey, InvocationContext, Next } from "@loopback/core"; -import { MiddlewareContext } from "@loopback/rest"; -import type { SessionContext as Context } from "../../../framework/loopback/framework"; -import { LoopbackRequest, LoopbackResponse } from "../../../framework/loopback/framework"; - -export function verifySession(options?: VerifySessionOptions): InterceptorOrKey { - return async (ctx: InvocationContext, next: Next) => { - let sessionRecipe = Session.getInstanceOrThrowError(); - let middlewareCtx = await ctx.get("middleware.http.context"); - let request = new LoopbackRequest(middlewareCtx); - let response = new LoopbackResponse(middlewareCtx); - (middlewareCtx as Context).session = await sessionRecipe.verifySession(options, request, response); - return await next(); - }; -} diff --git a/lib/ts/recipe/session/index.ts b/lib/ts/recipe/session/index.ts deleted file mode 100644 index 103ff4f73..000000000 --- a/lib/ts/recipe/session/index.ts +++ /dev/null @@ -1,424 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import SuperTokensError from "./error"; -import { - VerifySessionOptions, - RecipeInterface, - SessionContainerInterface as SessionContainer, - SessionInformation, - APIInterface, - APIOptions, - SessionClaimValidator, - SessionClaim, - ClaimValidationError, -} from "./types"; -import OpenIdRecipe from "../openid/recipe"; -import Recipe from "./recipe"; -import { JSONObject } from "../../types"; -import frameworks from "../../framework"; -import SuperTokens from "../../supertokens"; -import { getRequiredClaimValidators } from "./utils"; - -// For Express -export default class SessionWrapper { - static init = Recipe.init; - - static Error = SuperTokensError; - - static async createNewSession( - req: any, - res: any, - userId: string, - accessTokenPayload: any = {}, - sessionData: any = {}, - userContext: any = {} - ) { - const claimsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimsAddedByOtherRecipes(); - - let finalAccessTokenPayload = accessTokenPayload; - - for (const claim of claimsAddedByOtherRecipes) { - const update = await claim.build(userId, userContext); - finalAccessTokenPayload = { - ...finalAccessTokenPayload, - ...update, - }; - } - - if (!req.wrapperUsed) { - req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req); - } - - if (!res.wrapperUsed) { - res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res); - } - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewSession({ - req, - res, - userId, - accessTokenPayload: finalAccessTokenPayload, - sessionData, - userContext, - }); - } - - static async validateClaimsForSessionHandle( - sessionHandle: string, - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - sessionInfo: SessionInformation, - userContext: any - ) => Promise | SessionClaimValidator[], - userContext: any = {} - ): Promise< - | { - status: "SESSION_DOES_NOT_EXIST_ERROR"; - } - | { - status: "OK"; - invalidClaims: ClaimValidationError[]; - } - > { - const recipeImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; - - const sessionInfo = await recipeImpl.getSessionInformation({ - sessionHandle, - userContext, - }); - if (sessionInfo === undefined) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - - const claimValidatorsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators: SessionClaimValidator[] = await recipeImpl.getGlobalClaimValidators({ - userId: sessionInfo?.userId, - claimValidatorsAddedByOtherRecipes, - userContext, - }); - - const claimValidators = - overrideGlobalClaimValidators !== undefined - ? await overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, userContext) - : globalClaimValidators; - - let claimValidationResponse = await recipeImpl.validateClaims({ - userId: sessionInfo.userId, - accessTokenPayload: sessionInfo.accessTokenPayload, - claimValidators, - userContext, - }); - - if (claimValidationResponse.accessTokenPayloadUpdate !== undefined) { - if ( - !(await recipeImpl.mergeIntoAccessTokenPayload({ - sessionHandle, - accessTokenPayloadUpdate: claimValidationResponse.accessTokenPayloadUpdate, - userContext, - })) - ) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - } - return { - status: "OK", - invalidClaims: claimValidationResponse.invalidClaims, - }; - } - - static async validateClaimsInJWTPayload( - userId: string, - jwtPayload: JSONObject, - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - userId: string, - userContext: any - ) => Promise | SessionClaimValidator[], - userContext: any = {} - ): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }> { - const recipeImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; - - const claimValidatorsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators: SessionClaimValidator[] = await recipeImpl.getGlobalClaimValidators({ - userId, - claimValidatorsAddedByOtherRecipes, - userContext, - }); - - const claimValidators = - overrideGlobalClaimValidators !== undefined - ? await overrideGlobalClaimValidators(globalClaimValidators, userId, userContext) - : globalClaimValidators; - return recipeImpl.validateClaimsInJWTPayload({ - userId, - jwtPayload, - claimValidators, - userContext, - }); - } - - static getSession(req: any, res: any): Promise; - static getSession( - req: any, - res: any, - options?: VerifySessionOptions & { sessionRequired?: true }, - userContext?: any - ): Promise; - static getSession( - req: any, - res: any, - options?: VerifySessionOptions & { sessionRequired: false }, - userContext?: any - ): Promise; - static async getSession(req: any, res: any, options?: VerifySessionOptions, userContext: any = {}) { - if (!res.wrapperUsed) { - res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req); - } - const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; - const session = await recipeInterfaceImpl.getSession({ req, res, options, userContext }); - - if (session !== undefined) { - const claimValidators = await getRequiredClaimValidators( - session, - options?.overrideGlobalClaimValidators, - userContext - ); - await session.assertClaims(claimValidators, userContext); - } - return session; - } - - static getSessionInformation(sessionHandle: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getSessionInformation({ - sessionHandle, - userContext, - }); - } - - static refreshSession(req: any, res: any, userContext: any = {}) { - if (!res.wrapperUsed) { - res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req); - } - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.refreshSession({ req, res, userContext }); - } - - static revokeAllSessionsForUser(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllSessionsForUser({ userId, userContext }); - } - - static getAllSessionHandlesForUser(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getAllSessionHandlesForUser({ - userId, - userContext, - }); - } - - static revokeSession(sessionHandle: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeSession({ sessionHandle, userContext }); - } - - static revokeMultipleSessions(sessionHandles: string[], userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeMultipleSessions({ - sessionHandles, - userContext, - }); - } - - static updateSessionData(sessionHandle: string, newSessionData: any, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateSessionData({ - sessionHandle, - newSessionData, - userContext, - }); - } - - static regenerateAccessToken(accessToken: string, newAccessTokenPayload?: any, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.regenerateAccessToken({ - accessToken, - newAccessTokenPayload, - userContext, - }); - } - - static updateAccessTokenPayload(sessionHandle: string, newAccessTokenPayload: any, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateAccessTokenPayload({ - sessionHandle, - newAccessTokenPayload, - userContext, - }); - } - - static mergeIntoAccessTokenPayload( - sessionHandle: string, - accessTokenPayloadUpdate: JSONObject, - userContext: any = {} - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.mergeIntoAccessTokenPayload({ - sessionHandle, - accessTokenPayloadUpdate, - userContext, - }); - } - - static createJWT(payload?: any, validitySeconds?: number, userContext: any = {}) { - let openIdRecipe: OpenIdRecipe | undefined = Recipe.getInstanceOrThrowError().openIdRecipe; - - if (openIdRecipe !== undefined) { - return openIdRecipe.recipeImplementation.createJWT({ payload, validitySeconds, userContext }); - } - - throw new global.Error( - "createJWT cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ); - } - - static getJWKS(userContext: any = {}) { - let openIdRecipe: OpenIdRecipe | undefined = Recipe.getInstanceOrThrowError().openIdRecipe; - - if (openIdRecipe !== undefined) { - return openIdRecipe.recipeImplementation.getJWKS({ userContext }); - } - - throw new global.Error( - "getJWKS cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ); - } - - static getOpenIdDiscoveryConfiguration(userContext: any = {}) { - let openIdRecipe: OpenIdRecipe | undefined = Recipe.getInstanceOrThrowError().openIdRecipe; - - if (openIdRecipe !== undefined) { - return openIdRecipe.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); - } - - throw new global.Error( - "getOpenIdDiscoveryConfiguration cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ); - } - - static fetchAndSetClaim(sessionHandle: string, claim: SessionClaim, userContext: any = {}): Promise { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.fetchAndSetClaim({ - sessionHandle, - claim, - userContext, - }); - } - - static setClaimValue( - sessionHandle: string, - claim: SessionClaim, - value: T, - userContext: any = {} - ): Promise { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.setClaimValue({ - sessionHandle, - claim, - value, - userContext, - }); - } - - static getClaimValue( - sessionHandle: string, - claim: SessionClaim, - userContext: any = {} - ): Promise< - | { - status: "SESSION_DOES_NOT_EXIST_ERROR"; - } - | { - status: "OK"; - value: T | undefined; - } - > { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getClaimValue({ - sessionHandle, - claim, - userContext, - }); - } - - static removeClaim(sessionHandle: string, claim: SessionClaim, userContext: any = {}): Promise { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removeClaim({ - sessionHandle, - claim, - userContext, - }); - } -} - -export let init = SessionWrapper.init; - -export let createNewSession = SessionWrapper.createNewSession; - -export let getSession = SessionWrapper.getSession; - -export let getSessionInformation = SessionWrapper.getSessionInformation; - -export let refreshSession = SessionWrapper.refreshSession; - -export let revokeAllSessionsForUser = SessionWrapper.revokeAllSessionsForUser; - -export let getAllSessionHandlesForUser = SessionWrapper.getAllSessionHandlesForUser; - -export let revokeSession = SessionWrapper.revokeSession; - -export let revokeMultipleSessions = SessionWrapper.revokeMultipleSessions; - -export let updateSessionData = SessionWrapper.updateSessionData; - -export let updateAccessTokenPayload = SessionWrapper.updateAccessTokenPayload; -export let mergeIntoAccessTokenPayload = SessionWrapper.mergeIntoAccessTokenPayload; - -export let fetchAndSetClaim = SessionWrapper.fetchAndSetClaim; -export let setClaimValue = SessionWrapper.setClaimValue; -export let getClaimValue = SessionWrapper.getClaimValue; -export let removeClaim = SessionWrapper.removeClaim; -export let validateClaimsInJWTPayload = SessionWrapper.validateClaimsInJWTPayload; -export let validateClaimsForSessionHandle = SessionWrapper.validateClaimsForSessionHandle; - -export let Error = SessionWrapper.Error; - -// JWT Functions -export let createJWT = SessionWrapper.createJWT; - -export let getJWKS = SessionWrapper.getJWKS; - -// Open id functions - -export let getOpenIdDiscoveryConfiguration = SessionWrapper.getOpenIdDiscoveryConfiguration; - -export type { - VerifySessionOptions, - RecipeInterface, - SessionContainer, - APIInterface, - APIOptions, - SessionInformation, - SessionClaimValidator, -}; diff --git a/lib/ts/recipe/session/jwt.ts b/lib/ts/recipe/session/jwt.ts deleted file mode 100644 index f8ebf1f2b..000000000 --- a/lib/ts/recipe/session/jwt.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import * as crypto from "crypto"; - -const HEADERS = new Set([ - Buffer.from( - JSON.stringify({ - alg: "RS256", - typ: "JWT", - version: "1", - }) - ).toString("base64"), - Buffer.from( - JSON.stringify({ - alg: "RS256", - typ: "JWT", - version: "2", - }) - ).toString("base64"), -]); - -export type ParsedJWTInfo = { - rawTokenString: string; - rawPayload: string; - header: string; - payload: any; - signature: string; -}; - -export function parseJWTWithoutSignatureVerification(jwt: string): ParsedJWTInfo { - const splittedInput = jwt.split("."); - if (splittedInput.length !== 3) { - throw new Error("Invalid JWT"); - } - - // checking header - if (!HEADERS.has(splittedInput[0])) { - throw new Error("JWT header mismatch"); - } - - return { - rawTokenString: jwt, - rawPayload: splittedInput[1], - header: splittedInput[0], - // Ideally we would only parse this after the signature verification is done. - // We do this at the start, since we want to check if a token can be a supertokens access token or not - payload: JSON.parse(Buffer.from(splittedInput[1], "base64").toString()), - signature: splittedInput[2], - }; -} - -export function verifyJWT({ header, rawPayload, signature }: ParsedJWTInfo, jwtSigningPublicKey: string): void { - let verifier = crypto.createVerify("sha256"); - // convert the jwtSigningPublicKey into .pem format - - verifier.update(header + "." + rawPayload); - if ( - !verifier.verify( - "-----BEGIN PUBLIC KEY-----\n" + jwtSigningPublicKey + "\n-----END PUBLIC KEY-----", - signature, - "base64" - ) - ) { - throw new Error("JWT verification failed"); - } -} diff --git a/lib/ts/recipe/session/recipe.ts b/lib/ts/recipe/session/recipe.ts deleted file mode 100644 index b52dde197..000000000 --- a/lib/ts/recipe/session/recipe.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import RecipeModule from "../../recipeModule"; -import { - TypeInput, - TypeNormalisedInput, - RecipeInterface, - APIInterface, - VerifySessionOptions, - SessionClaimValidator, - SessionClaim, -} from "./types"; -import STError from "./error"; -import { validateAndNormaliseUserInput } from "./utils"; -import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod } from "../../types"; -import handleRefreshAPI from "./api/refresh"; -import signOutAPI from "./api/signout"; -import { REFRESH_API_PATH, SIGNOUT_API_PATH } from "./constants"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { - clearSessionFromAllTokenTransferMethods, - getCORSAllowedHeaders as getCORSAllowedHeadersFromCookiesAndHeaders, -} from "./cookieAndHeaders"; -import RecipeImplementation from "./recipeImplementation"; -import RecipeImplementationWithJWT from "./with-jwt"; -import { Querier } from "../../querier"; -import APIImplementation from "./api/implementation"; -import { BaseRequest, BaseResponse } from "../../framework"; -import OverrideableBuilder from "supertokens-js-override"; -import { APIOptions } from "."; -import OpenIdRecipe from "../openid/recipe"; -import { logDebugMessage } from "../../logger"; -import { makeDefaultUserContextFromAPI } from "../../utils"; - -// For Express -export default class SessionRecipe extends RecipeModule { - private static instance: SessionRecipe | undefined = undefined; - static RECIPE_ID = "session"; - - private claimsAddedByOtherRecipes: SessionClaim[] = []; - private claimValidatorsAddedByOtherRecipes: SessionClaimValidator[] = []; - - config: TypeNormalisedInput; - - recipeInterfaceImpl: RecipeInterface; - openIdRecipe?: OpenIdRecipe; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(this, appInfo, config); - logDebugMessage("session init: antiCsrf: " + this.config.antiCsrf); - logDebugMessage("session init: cookieDomain: " + this.config.cookieDomain); - logDebugMessage("session init: cookieSameSite: " + this.config.cookieSameSite); - logDebugMessage("session init: cookieSecure: " + this.config.cookieSecure); - logDebugMessage("session init: refreshTokenPath: " + this.config.refreshTokenPath.getAsStringDangerous()); - logDebugMessage("session init: sessionExpiredStatusCode: " + this.config.sessionExpiredStatusCode); - - this.isInServerlessEnv = isInServerlessEnv; - - if (this.config.jwt.enable === true) { - this.openIdRecipe = new OpenIdRecipe(recipeId, appInfo, isInServerlessEnv, { - issuer: this.config.jwt.issuer, - override: this.config.override.openIdFeature, - }); - - let builder = new OverrideableBuilder( - RecipeImplementation( - Querier.getNewInstanceOrThrowError(recipeId), - this.config, - this.getAppInfo(), - () => this.recipeInterfaceImpl - ) - ); - this.recipeInterfaceImpl = builder - .override((oI) => { - return RecipeImplementationWithJWT( - oI, - // this.jwtRecipe is never undefined here - this.openIdRecipe!.recipeImplementation, - this.config - ); - }) - .override(this.config.override.functions) - .build(); - } else { - { - let builder = new OverrideableBuilder( - RecipeImplementation( - Querier.getNewInstanceOrThrowError(recipeId), - this.config, - this.getAppInfo(), - () => this.recipeInterfaceImpl - ) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - } - - static getInstanceOrThrowError(): SessionRecipe { - if (SessionRecipe.instance !== undefined) { - return SessionRecipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (SessionRecipe.instance === undefined) { - SessionRecipe.instance = new SessionRecipe(SessionRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return SessionRecipe.instance; - } else { - throw new Error("Session recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - SessionRecipe.instance = undefined; - } - - addClaimFromOtherRecipe = (claim: SessionClaim) => { - // We are throwing here (and not in addClaimValidatorFromOtherRecipe) because if multiple - // claims are added with the same key they will overwrite each other. Validators will all run - // and work as expected even if they are added multiple times. - if (this.claimsAddedByOtherRecipes.some((c) => c.key === claim.key)) { - throw new Error("Claim added by multiple recipes"); - } - this.claimsAddedByOtherRecipes.push(claim); - }; - - getClaimsAddedByOtherRecipes = (): SessionClaim[] => { - return this.claimsAddedByOtherRecipes; - }; - - addClaimValidatorFromOtherRecipe = (builder: SessionClaimValidator) => { - this.claimValidatorsAddedByOtherRecipes.push(builder); - }; - - getClaimValidatorsAddedByOtherRecipes = (): SessionClaimValidator[] => { - return this.claimValidatorsAddedByOtherRecipes; - }; - - // abstract instance functions below............... - - getAPIsHandled = (): APIHandled[] => { - let apisHandled: APIHandled[] = [ - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(REFRESH_API_PATH), - id: REFRESH_API_PATH, - disabled: this.apiImpl.refreshPOST === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(SIGNOUT_API_PATH), - id: SIGNOUT_API_PATH, - disabled: this.apiImpl.signOutPOST === undefined, - }, - ]; - - if (this.openIdRecipe !== undefined) { - apisHandled.push(...this.openIdRecipe.getAPIsHandled()); - } - - return apisHandled; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ): Promise => { - let options: APIOptions = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - req, - res, - }; - if (id === REFRESH_API_PATH) { - return await handleRefreshAPI(this.apiImpl, options); - } else if (id === SIGNOUT_API_PATH) { - return await signOutAPI(this.apiImpl, options); - } else if (this.openIdRecipe !== undefined) { - return await this.openIdRecipe.handleAPIRequest(id, req, res, path, method); - } else { - return false; - } - }; - - handleError = async (err: STError, request: BaseRequest, response: BaseResponse) => { - if (err.fromRecipe === SessionRecipe.RECIPE_ID) { - if (err.type === STError.UNAUTHORISED) { - logDebugMessage("errorHandler: returning UNAUTHORISED"); - if ( - err.payload === undefined || - err.payload.clearTokens === undefined || - err.payload.clearTokens === true - ) { - logDebugMessage("errorHandler: Clearing tokens because of UNAUTHORISED response"); - clearSessionFromAllTokenTransferMethods(this.config, response); - } - return await this.config.errorHandlers.onUnauthorised(err.message, request, response); - } else if (err.type === STError.TRY_REFRESH_TOKEN) { - logDebugMessage("errorHandler: returning TRY_REFRESH_TOKEN"); - return await this.config.errorHandlers.onTryRefreshToken(err.message, request, response); - } else if (err.type === STError.TOKEN_THEFT_DETECTED) { - logDebugMessage("errorHandler: returning TOKEN_THEFT_DETECTED"); - logDebugMessage("errorHandler: Clearing tokens because of TOKEN_THEFT_DETECTED response"); - clearSessionFromAllTokenTransferMethods(this.config, response); - return await this.config.errorHandlers.onTokenTheftDetected( - err.payload.sessionHandle, - err.payload.userId, - request, - response - ); - } else if (err.type === STError.INVALID_CLAIMS) { - return await this.config.errorHandlers.onInvalidClaim(err.payload, request, response); - } else { - throw err; - } - } else if (this.openIdRecipe !== undefined) { - return await this.openIdRecipe.handleError(err, request, response); - } else { - throw err; - } - }; - - getAllCORSHeaders = (): string[] => { - let corsHeaders: string[] = [...getCORSAllowedHeadersFromCookiesAndHeaders()]; - - if (this.openIdRecipe !== undefined) { - corsHeaders.push(...this.openIdRecipe.getAllCORSHeaders()); - } - - return corsHeaders; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return ( - STError.isErrorFromSuperTokens(err) && - (err.fromRecipe === SessionRecipe.RECIPE_ID || - (this.openIdRecipe !== undefined && this.openIdRecipe.isErrorFromThisRecipe(err))) - ); - }; - - verifySession = async (options: VerifySessionOptions | undefined, request: BaseRequest, response: BaseResponse) => { - return await this.apiImpl.verifySession({ - verifySessionOptions: options, - options: { - config: this.config, - req: request, - res: response, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - }, - userContext: makeDefaultUserContextFromAPI(request), - }); - }; -} diff --git a/lib/ts/recipe/session/recipeImplementation.ts b/lib/ts/recipe/session/recipeImplementation.ts deleted file mode 100644 index bd695b616..000000000 --- a/lib/ts/recipe/session/recipeImplementation.ts +++ /dev/null @@ -1,749 +0,0 @@ -import { - RecipeInterface, - VerifySessionOptions, - TypeNormalisedInput, - SessionInformation, - KeyInfo, - AntiCsrfType, - SessionClaimValidator, - SessionClaim, - ClaimValidationError, - TokenTransferMethod, -} from "./types"; -import * as SessionFunctions from "./sessionFunctions"; -import { - clearSession, - getAntiCsrfTokenFromHeaders, - setFrontTokenInHeaders, - getToken, - setToken, - setCookie, -} from "./cookieAndHeaders"; -import { attachTokensToResponse, validateClaimsInPayload } from "./utils"; -import Session from "./sessionClass"; -import STError from "./error"; -import { normaliseHttpMethod, getRidFromHeader, isAnIpAddress } from "../../utils"; -import { Querier } from "../../querier"; -import { PROCESS_STATE, ProcessState } from "../../processState"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { JSONObject, NormalisedAppinfo } from "../../types"; -import { logDebugMessage } from "../../logger"; -import { BaseResponse } from "../../framework/response"; -import { BaseRequest } from "../../framework/request"; -import { availableTokenTransferMethods } from "./constants"; -import { ParsedJWTInfo, parseJWTWithoutSignatureVerification } from "./jwt"; -import { validateAccessTokenStructure } from "./accessToken"; - -export class HandshakeInfo { - constructor( - public antiCsrf: AntiCsrfType, - public accessTokenBlacklistingEnabled: boolean, - public accessTokenValidity: number, - public refreshTokenValidity: number, - private rawJwtSigningPublicKeyList: KeyInfo[] - ) {} - - setJwtSigningPublicKeyList(updatedList: KeyInfo[]) { - this.rawJwtSigningPublicKeyList = updatedList; - } - - getJwtSigningPublicKeyList() { - return this.rawJwtSigningPublicKeyList.filter((key) => key.expiryTime > Date.now()); - } - - clone() { - return new HandshakeInfo( - this.antiCsrf, - this.accessTokenBlacklistingEnabled, - this.accessTokenValidity, - this.refreshTokenValidity, - this.rawJwtSigningPublicKeyList - ); - } -} - -export type Helpers = { - querier: Querier; - getHandshakeInfo: (forceRefetch?: boolean) => Promise; - updateJwtSigningPublicKeyInfo: (keyList: KeyInfo[] | undefined, publicKey: string, expiryTime: number) => void; - config: TypeNormalisedInput; - appInfo: NormalisedAppinfo; - getRecipeImpl: () => RecipeInterface; -}; - -// We are defining this here to reduce the scope of legacy code -const LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken"; - -export default function getRecipeInterface( - querier: Querier, - config: TypeNormalisedInput, - appInfo: NormalisedAppinfo, - getRecipeImplAfterOverrides: () => RecipeInterface -): RecipeInterface { - let handshakeInfo: undefined | HandshakeInfo; - - async function getHandshakeInfo(forceRefetch = false): Promise { - if (handshakeInfo === undefined || handshakeInfo.getJwtSigningPublicKeyList().length === 0 || forceRefetch) { - let antiCsrf = config.antiCsrf; - ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO); - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/handshake"), {}); - - handshakeInfo = new HandshakeInfo( - antiCsrf, - response.accessTokenBlacklistingEnabled, - response.accessTokenValidity, - response.refreshTokenValidity, - response.jwtSigningPublicKeyList - ); - - updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - } - return handshakeInfo; - } - - function updateJwtSigningPublicKeyInfo(keyList: KeyInfo[] | undefined, publicKey: string, expiryTime: number) { - if (keyList === undefined) { - // Setting createdAt to Date.now() emulates the old lastUpdatedAt logic - keyList = [{ publicKey, expiryTime, createdAt: Date.now() }]; - } - - if (handshakeInfo !== undefined) { - handshakeInfo.setJwtSigningPublicKeyList(keyList); - } - } - - let obj: RecipeInterface = { - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload = {}, - sessionData = {}, - userContext, - }: { - req: BaseRequest; - res: BaseResponse; - userId: string; - accessTokenPayload?: any; - sessionData?: any; - userContext: any; - }): Promise { - logDebugMessage("createNewSession: Started"); - let outputTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: true, userContext }); - if (outputTransferMethod === "any") { - outputTransferMethod = "header"; - } - logDebugMessage("createNewSession: using transfer method " + outputTransferMethod); - - if ( - outputTransferMethod === "cookie" && - helpers.config.cookieSameSite === "none" && - !helpers.config.cookieSecure && - !( - (helpers.appInfo.topLevelAPIDomain === "localhost" || - isAnIpAddress(helpers.appInfo.topLevelAPIDomain)) && - (helpers.appInfo.topLevelWebsiteDomain === "localhost" || - isAnIpAddress(helpers.appInfo.topLevelWebsiteDomain)) - ) - ) { - // We can allow insecure cookie when both website & API domain are localhost or an IP - // When either of them is a different domain, API domain needs to have https and a secure cookie to work - throw new Error( - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." - ); - } - - const disableAntiCSRF = outputTransferMethod === "header"; - - let response = await SessionFunctions.createNewSession( - helpers, - userId, - disableAntiCSRF, - accessTokenPayload, - sessionData - ); - - for (const transferMethod of availableTokenTransferMethods) { - if (transferMethod !== outputTransferMethod && getToken(req, "access", transferMethod) !== undefined) { - clearSession(config, res, transferMethod); - } - } - - attachTokensToResponse(config, res, response, outputTransferMethod); - return new Session( - helpers, - response.accessToken.token, - response.session.handle, - response.session.userId, - response.session.userDataInJWT, - res, - req, - outputTransferMethod - ); - }, - - getGlobalClaimValidators: async function (input: { - claimValidatorsAddedByOtherRecipes: SessionClaimValidator[]; - }) { - return input.claimValidatorsAddedByOtherRecipes; - }, - - /* In all cases if sIdRefreshToken token exists (so it's a legacy session) we return TRY_REFRESH_TOKEN. The refresh endpoint will clear this cookie and try to upgrade the session. - Check https://supertokens.com/docs/contribute/decisions/session/0007 for further details and a table of expected behaviours - */ - getSession: async function ({ - req, - res, - options, - userContext, - }: { - req: BaseRequest; - res: BaseResponse; - options?: VerifySessionOptions; - userContext: any; - }): Promise { - logDebugMessage("getSession: Started"); - - // This token isn't handled by getToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - // This could create a spike on refresh calls during the update of the backend SDK - throw new STError({ - message: "using legacy session, please call the refresh API", - type: STError.TRY_REFRESH_TOKEN, - }); - } - - const sessionOptional = options?.sessionRequired === false; - logDebugMessage("getSession: optional validation: " + sessionOptional); - - const accessTokens: { - [key in TokenTransferMethod]?: ParsedJWTInfo; - } = {}; - - // We check all token transfer methods for available access tokens - for (const transferMethod of availableTokenTransferMethods) { - const tokenString = getToken(req, "access", transferMethod); - if (tokenString !== undefined) { - try { - const info = parseJWTWithoutSignatureVerification(tokenString); - validateAccessTokenStructure(info.payload); - logDebugMessage("getSession: got access token from " + transferMethod); - accessTokens[transferMethod] = info; - } catch { - logDebugMessage( - `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` - ); - } - } - } - - const allowedTransferMethod = config.getTokenTransferMethod({ - req, - forCreateNewSession: false, - userContext, - }); - let requestTransferMethod: TokenTransferMethod; - let accessToken: ParsedJWTInfo | undefined; - - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - accessTokens["header"] !== undefined - ) { - logDebugMessage("getSession: using header transfer method"); - requestTransferMethod = "header"; - accessToken = accessTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - accessTokens["cookie"] !== undefined - ) { - logDebugMessage("getSession: using cookie transfer method"); - requestTransferMethod = "cookie"; - accessToken = accessTokens["cookie"]; - } else { - if (sessionOptional) { - logDebugMessage( - "getSession: returning undefined because accessToken is undefined and sessionRequired is false" - ); - // there is no session that exists here, and the user wants session verification - // to be optional. So we return undefined. - return undefined; - } - - logDebugMessage("getSession: UNAUTHORISED because accessToken in request is undefined"); - throw new STError({ - message: - "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", - type: STError.UNAUTHORISED, - payload: { - // we do not clear the session here because of a - // race condition mentioned here: https://github.com/supertokens/supertokens-node/issues/17 - clearTokens: false, - }, - }); - } - - let antiCsrfToken = getAntiCsrfTokenFromHeaders(req); - let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; - - if (doAntiCsrfCheck === undefined) { - doAntiCsrfCheck = normaliseHttpMethod(req.getMethod()) !== "get"; - } - - if (requestTransferMethod === "header") { - doAntiCsrfCheck = false; - } - - logDebugMessage("getSession: Value of doAntiCsrfCheck is: " + doAntiCsrfCheck); - - let response = await SessionFunctions.getSession( - helpers, - accessToken, - antiCsrfToken, - doAntiCsrfCheck, - getRidFromHeader(req) !== undefined - ); - let accessTokenString = accessToken.rawTokenString; - if (response.accessToken !== undefined) { - setFrontTokenInHeaders( - res, - response.session.userId, - response.accessToken.expiry, - response.session.userDataInJWT - ); - setToken( - config, - res, - "access", - response.accessToken.token, - // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. - // Even if the token is expired the presence of the token indicates that the user could have a valid refresh - // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. - Date.now() + 3153600000000, - requestTransferMethod - ); - accessTokenString = response.accessToken.token; - } - logDebugMessage("getSession: Success!"); - const session = new Session( - helpers, - accessTokenString, - response.session.handle, - response.session.userId, - response.session.userDataInJWT, - res, - req, - requestTransferMethod - ); - - return session; - }, - - validateClaims: async function ( - this: RecipeInterface, - input: { - userId: string; - accessTokenPayload: any; - claimValidators: SessionClaimValidator[]; - userContext: any; - } - ): Promise<{ - invalidClaims: ClaimValidationError[]; - accessTokenPayloadUpdate?: any; - }> { - let accessTokenPayload = input.accessTokenPayload; - let accessTokenPayloadUpdate = undefined; - const origSessionClaimPayloadJSON = JSON.stringify(accessTokenPayload); - - for (const validator of input.claimValidators) { - logDebugMessage("updateClaimsInPayloadIfNeeded checking shouldRefetch for " + validator.id); - if ("claim" in validator && (await validator.shouldRefetch(accessTokenPayload, input.userContext))) { - logDebugMessage("updateClaimsInPayloadIfNeeded refetching " + validator.id); - const value = await validator.claim.fetchValue(input.userId, input.userContext); - logDebugMessage( - "updateClaimsInPayloadIfNeeded " + validator.id + " refetch result " + JSON.stringify(value) - ); - if (value !== undefined) { - accessTokenPayload = validator.claim.addToPayload_internal( - accessTokenPayload, - value, - input.userContext - ); - } - } - } - - if (JSON.stringify(accessTokenPayload) !== origSessionClaimPayloadJSON) { - accessTokenPayloadUpdate = accessTokenPayload; - } - - const invalidClaims = await validateClaimsInPayload( - input.claimValidators, - accessTokenPayload, - input.userContext - ); - - return { - invalidClaims, - accessTokenPayloadUpdate, - }; - }, - - validateClaimsInJWTPayload: async function ( - this: RecipeInterface, - input: { - userId: string; - jwtPayload: JSONObject; - claimValidators: SessionClaimValidator[]; - userContext: any; - } - ): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }> { - // We skip refetching here, because we have no way of updating the JWT payload here - // if we have access to the entire session other methods can be used to do validation while updating - const invalidClaims = await validateClaimsInPayload( - input.claimValidators, - input.jwtPayload, - input.userContext - ); - - return { - status: "OK", - invalidClaims, - }; - }, - - getSessionInformation: async function ({ - sessionHandle, - }: { - sessionHandle: string; - }): Promise { - return SessionFunctions.getSessionInformation(helpers, sessionHandle); - }, - - /* - In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. - Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours - */ - refreshSession: async function ( - this: RecipeInterface, - { req, res, userContext }: { req: BaseRequest; res: BaseResponse; userContext: any } - ): Promise { - logDebugMessage("refreshSession: Started"); - - const refreshTokens: { - [key in TokenTransferMethod]?: string; - } = {}; - - // We check all token transfer methods for available refresh tokens - // We do this so that we can later clear all we are not overwriting - for (const transferMethod of availableTokenTransferMethods) { - refreshTokens[transferMethod] = getToken(req, "refresh", transferMethod); - if (refreshTokens[transferMethod] !== undefined) { - logDebugMessage("refreshSession: got refresh token from " + transferMethod); - } - } - - const allowedTransferMethod = config.getTokenTransferMethod({ - req, - forCreateNewSession: false, - userContext, - }); - logDebugMessage("refreshSession: getTokenTransferMethod returned " + allowedTransferMethod); - - let requestTransferMethod: TokenTransferMethod; - let refreshToken: string | undefined; - - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - refreshTokens["header"] !== undefined - ) { - logDebugMessage("refreshSession: using header transfer method"); - requestTransferMethod = "header"; - refreshToken = refreshTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - refreshTokens["cookie"] - ) { - logDebugMessage("refreshSession: using cookie transfer method"); - requestTransferMethod = "cookie"; - refreshToken = refreshTokens["cookie"]; - } else { - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logDebugMessage( - "refreshSession: cleared legacy id refresh token because refresh token was not found" - ); - setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath"); - } - - logDebugMessage("refreshSession: UNAUTHORISED because refresh token in request is undefined"); - throw new STError({ - message: "Refresh token not found. Are you sending the refresh token in the request?", - payload: { - clearTokens: false, - }, - type: STError.UNAUTHORISED, - }); - } - - try { - let antiCsrfToken = getAntiCsrfTokenFromHeaders(req); - let response = await SessionFunctions.refreshSession( - helpers, - refreshToken, - antiCsrfToken, - getRidFromHeader(req) !== undefined, - requestTransferMethod - ); - logDebugMessage("refreshSession: Attaching refreshed session info as " + requestTransferMethod); - - // We clear the tokens in all token transfer methods we are not going to overwrite - for (const transferMethod of availableTokenTransferMethods) { - if (transferMethod !== requestTransferMethod && refreshTokens[transferMethod] !== undefined) { - clearSession(config, res, transferMethod); - } - } - - attachTokensToResponse(config, res, response, requestTransferMethod); - - logDebugMessage("refreshSession: Success!"); - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logDebugMessage("refreshSession: cleared legacy id refresh token after successful refresh"); - setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath"); - } - - return new Session( - helpers, - response.accessToken.token, - response.session.handle, - response.session.userId, - response.session.userDataInJWT, - res, - req, - requestTransferMethod - ); - } catch (err) { - if (err.type === STError.TOKEN_THEFT_DETECTED || err.payload.clearTokens) { - // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { - logDebugMessage( - "refreshSession: cleared legacy id refresh token because refresh is clearing other tokens" - ); - setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, "", 0, "accessTokenPath"); - } - } - throw err; - } - }, - - regenerateAccessToken: async function ( - this: RecipeInterface, - input: { - accessToken: string; - newAccessTokenPayload?: any; - userContext: any; - } - ): Promise< - | { - status: "OK"; - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken?: { - token: string; - expiry: number; - createdTime: number; - }; - } - | undefined - > { - let newAccessTokenPayload = - input.newAccessTokenPayload === null || input.newAccessTokenPayload === undefined - ? {} - : input.newAccessTokenPayload; - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/session/regenerate"), { - accessToken: input.accessToken, - userDataInJWT: newAccessTokenPayload, - }); - if (response.status === "UNAUTHORISED") { - return undefined; - } - return response; - }, - - revokeAllSessionsForUser: function ({ userId }: { userId: string }) { - return SessionFunctions.revokeAllSessionsForUser(helpers, userId); - }, - - getAllSessionHandlesForUser: function ({ userId }: { userId: string }): Promise { - return SessionFunctions.getAllSessionHandlesForUser(helpers, userId); - }, - - revokeSession: function ({ sessionHandle }: { sessionHandle: string }): Promise { - return SessionFunctions.revokeSession(helpers, sessionHandle); - }, - - revokeMultipleSessions: function ({ sessionHandles }: { sessionHandles: string[] }) { - return SessionFunctions.revokeMultipleSessions(helpers, sessionHandles); - }, - - updateSessionData: function ({ - sessionHandle, - newSessionData, - }: { - sessionHandle: string; - newSessionData: any; - }): Promise { - return SessionFunctions.updateSessionData(helpers, sessionHandle, newSessionData); - }, - - updateAccessTokenPayload: function ({ - sessionHandle, - newAccessTokenPayload, - }: { - sessionHandle: string; - newAccessTokenPayload: any; - }): Promise { - return SessionFunctions.updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload); - }, - - mergeIntoAccessTokenPayload: async function ( - this: RecipeInterface, - { - sessionHandle, - accessTokenPayloadUpdate, - userContext, - }: { - sessionHandle: string; - accessTokenPayloadUpdate: JSONObject; - userContext: any; - } - ) { - const sessionInfo = await this.getSessionInformation({ sessionHandle, userContext }); - if (sessionInfo === undefined) { - return false; - } - const newAccessTokenPayload = { ...sessionInfo.accessTokenPayload, ...accessTokenPayloadUpdate }; - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete newAccessTokenPayload[key]; - } - } - return this.updateAccessTokenPayload({ sessionHandle, newAccessTokenPayload, userContext }); - }, - - getAccessTokenLifeTimeMS: async function (): Promise { - return (await getHandshakeInfo()).accessTokenValidity; - }, - - getRefreshTokenLifeTimeMS: async function (): Promise { - return (await getHandshakeInfo()).refreshTokenValidity; - }, - - fetchAndSetClaim: async function ( - this: RecipeInterface, - input: { - sessionHandle: string; - claim: SessionClaim; - userContext?: any; - } - ) { - const sessionInfo = await this.getSessionInformation({ - sessionHandle: input.sessionHandle, - userContext: input.userContext, - }); - if (sessionInfo === undefined) { - return false; - } - const accessTokenPayloadUpdate = await input.claim.build(sessionInfo.userId, input.userContext); - - return this.mergeIntoAccessTokenPayload({ - sessionHandle: input.sessionHandle, - accessTokenPayloadUpdate, - userContext: input.userContext, - }); - }, - - setClaimValue: function ( - this: RecipeInterface, - input: { - sessionHandle: string; - claim: SessionClaim; - value: T; - userContext?: any; - } - ) { - const accessTokenPayloadUpdate = input.claim.addToPayload_internal({}, input.value, input.userContext); - return this.mergeIntoAccessTokenPayload({ - sessionHandle: input.sessionHandle, - accessTokenPayloadUpdate, - userContext: input.userContext, - }); - }, - - getClaimValue: async function ( - this: RecipeInterface, - input: { sessionHandle: string; claim: SessionClaim; userContext?: any } - ) { - const sessionInfo = await this.getSessionInformation({ - sessionHandle: input.sessionHandle, - userContext: input.userContext, - }); - - if (sessionInfo === undefined) { - return { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }; - } - - return { - status: "OK", - value: input.claim.getValueFromPayload(sessionInfo.accessTokenPayload, input.userContext), - }; - }, - - removeClaim: function ( - this: RecipeInterface, - input: { sessionHandle: string; claim: SessionClaim; userContext?: any } - ) { - const accessTokenPayloadUpdate = input.claim.removeFromPayloadByMerge_internal({}, input.userContext); - - return this.mergeIntoAccessTokenPayload({ - sessionHandle: input.sessionHandle, - accessTokenPayloadUpdate, - userContext: input.userContext, - }); - }, - }; - - let helpers: Helpers = { - querier, - updateJwtSigningPublicKeyInfo, - getHandshakeInfo, - config, - appInfo, - getRecipeImpl: getRecipeImplAfterOverrides, - }; - - if (process.env.TEST_MODE === "testing") { - // testing mode, we add some of the help functions to the obj - (obj as any).getHandshakeInfo = getHandshakeInfo; - (obj as any).updateJwtSigningPublicKeyInfo = updateJwtSigningPublicKeyInfo; - (obj as any).helpers = helpers; - (obj as any).setHandshakeInfo = function (info: any) { - handshakeInfo = info; - }; - } - - return obj; -} diff --git a/lib/ts/recipe/session/sessionClass.ts b/lib/ts/recipe/session/sessionClass.ts deleted file mode 100644 index adce2766d..000000000 --- a/lib/ts/recipe/session/sessionClass.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { BaseRequest, BaseResponse } from "../../framework"; -import { clearSession, setFrontTokenInHeaders, setToken } from "./cookieAndHeaders"; -import STError from "./error"; -import { SessionClaim, SessionClaimValidator, SessionContainerInterface, TokenTransferMethod } from "./types"; -import { Helpers } from "./recipeImplementation"; - -export default class Session implements SessionContainerInterface { - constructor( - protected helpers: Helpers, - protected accessToken: string, - protected sessionHandle: string, - protected userId: string, - protected userDataInAccessToken: any, - protected res: BaseResponse, - protected readonly req: BaseRequest, - protected readonly transferMethod: TokenTransferMethod - ) {} - - async revokeSession(userContext?: any) { - await this.helpers.getRecipeImpl().revokeSession({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - - // we do not check the output of calling revokeSession - // before clearing the cookies because we are revoking the - // current API request's session. - // If we instead clear the cookies only when revokeSession - // returns true, it can cause this kind of a bug: - // https://github.com/supertokens/supertokens-node/issues/343 - clearSession(this.helpers.config, this.res, this.transferMethod); - } - - async getSessionData(userContext?: any): Promise { - let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - if (sessionInfo === undefined) { - throw new STError({ - message: "Session does not exist anymore", - type: STError.UNAUTHORISED, - }); - } - return sessionInfo.sessionData; - } - - async updateSessionData(newSessionData: any, userContext?: any) { - if ( - !(await this.helpers.getRecipeImpl().updateSessionData({ - sessionHandle: this.sessionHandle, - newSessionData, - userContext: userContext === undefined ? {} : userContext, - })) - ) { - throw new STError({ - message: "Session does not exist anymore", - type: STError.UNAUTHORISED, - }); - } - } - - getUserId(_userContext?: any) { - return this.userId; - } - - getAccessTokenPayload(_userContext?: any) { - return this.userDataInAccessToken; - } - - getHandle() { - return this.sessionHandle; - } - - getAccessToken() { - return this.accessToken; - } - - // Any update to this function should also be reflected in the respective JWT version - async mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: any, userContext?: any): Promise { - const updatedPayload = { ...this.getAccessTokenPayload(userContext), ...accessTokenPayloadUpdate }; - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete updatedPayload[key]; - } - } - - await this.updateAccessTokenPayload(updatedPayload, userContext); - } - - async getTimeCreated(userContext?: any): Promise { - let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - if (sessionInfo === undefined) { - throw new STError({ - message: "Session does not exist anymore", - type: STError.UNAUTHORISED, - }); - } - return sessionInfo.timeCreated; - } - - async getExpiry(userContext?: any): Promise { - let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ - sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, - }); - if (sessionInfo === undefined) { - throw new STError({ - message: "Session does not exist anymore", - type: STError.UNAUTHORISED, - }); - } - return sessionInfo.expiry; - } - - // Any update to this function should also be reflected in the respective JWT version - async assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise { - let validateClaimResponse = await this.helpers.getRecipeImpl().validateClaims({ - accessTokenPayload: this.getAccessTokenPayload(userContext), - userId: this.getUserId(userContext), - claimValidators, - userContext, - }); - - if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { - await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); - } - - if (validateClaimResponse.invalidClaims.length !== 0) { - throw new STError({ - type: "INVALID_CLAIMS", - message: "INVALID_CLAIMS", - payload: validateClaimResponse.invalidClaims, - }); - } - } - - // Any update to this function should also be reflected in the respective JWT version - async fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise { - const update = await claim.build(this.getUserId(userContext), userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - - // Any update to this function should also be reflected in the respective JWT version - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise { - const update = claim.addToPayload_internal({}, value, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - - // Any update to this function should also be reflected in the respective JWT version - async getClaimValue(claim: SessionClaim, userContext?: any) { - return claim.getValueFromPayload(await this.getAccessTokenPayload(userContext), userContext); - } - - // Any update to this function should also be reflected in the respective JWT version - removeClaim(claim: SessionClaim, userContext?: any): Promise { - const update = claim.removeFromPayloadByMerge_internal({}, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - - /** - * @deprecated Use mergeIntoAccessTokenPayload - */ - async updateAccessTokenPayload(newAccessTokenPayload: any, userContext: any): Promise { - let response = await this.helpers.getRecipeImpl().regenerateAccessToken({ - accessToken: this.getAccessToken(), - newAccessTokenPayload, - userContext: userContext === undefined ? {} : userContext, - }); - if (response === undefined) { - throw new STError({ - message: "Session does not exist anymore", - type: STError.UNAUTHORISED, - }); - } - this.userDataInAccessToken = response.session.userDataInJWT; - if (response.accessToken !== undefined) { - this.accessToken = response.accessToken.token; - setFrontTokenInHeaders( - this.res, - response.session.userId, - response.accessToken.expiry, - response.session.userDataInJWT - ); - setToken( - this.helpers.config, - this.res, - "access", - response.accessToken.token, - // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. - // Even if the token is expired the presence of the token indicates that the user could have a valid refresh - // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. - Date.now() + 3153600000000, - this.transferMethod - ); - } - } -} diff --git a/lib/ts/recipe/session/sessionFunctions.ts b/lib/ts/recipe/session/sessionFunctions.ts deleted file mode 100644 index 1a909bdec..000000000 --- a/lib/ts/recipe/session/sessionFunctions.ts +++ /dev/null @@ -1,437 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { getInfoFromAccessToken, sanitizeNumberInput } from "./accessToken"; -import { ParsedJWTInfo } from "./jwt"; -import STError from "./error"; -import { PROCESS_STATE, ProcessState } from "../../processState"; -import { CreateOrRefreshAPIResponse, SessionInformation, TokenTransferMethod } from "./types"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { Helpers } from "./recipeImplementation"; -import { maxVersion } from "../../utils"; -import { logDebugMessage } from "../../logger"; - -/** - * @description call this to "login" a user. - */ -export async function createNewSession( - helpers: Helpers, - userId: string, - disableAntiCsrf: boolean, - accessTokenPayload: any = {}, - sessionData: any = {} -): Promise { - accessTokenPayload = accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; - sessionData = sessionData === null || sessionData === undefined ? {} : sessionData; - - let requestBody: { - userId: string; - userDataInJWT: any; - userDataInDatabase: any; - enableAntiCsrf?: boolean; - } = { - userId, - userDataInJWT: accessTokenPayload, - userDataInDatabase: sessionData, - }; - - let handShakeInfo = await helpers.getHandshakeInfo(); - requestBody.enableAntiCsrf = !disableAntiCsrf && handShakeInfo.antiCsrf === "VIA_TOKEN"; - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session"), requestBody); - helpers.updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - delete response.status; - delete response.jwtSigningPublicKey; - delete response.jwtSigningPublicKeyExpiryTime; - delete response.jwtSigningPublicKeyList; - - return response; -} - -/** - * @description authenticates a session. To be used in APIs that require authentication - */ -export async function getSession( - helpers: Helpers, - parsedAccessToken: ParsedJWTInfo, - antiCsrfToken: string | undefined, - doAntiCsrfCheck: boolean, - containsCustomHeader: boolean -): Promise<{ - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken?: { - token: string; - expiry: number; - createdTime: number; - }; -}> { - let handShakeInfo = await helpers.getHandshakeInfo(); - let accessTokenInfo; - - // If we have no key old enough to verify this access token we should reject it without calling the core - let foundASigningKeyThatIsOlderThanTheAccessToken = false; - for (const key of handShakeInfo.getJwtSigningPublicKeyList()) { - try { - /** - * get access token info using existing signingKey - */ - accessTokenInfo = await getInfoFromAccessToken( - parsedAccessToken, - key.publicKey, - handShakeInfo.antiCsrf === "VIA_TOKEN" && doAntiCsrfCheck - ); - foundASigningKeyThatIsOlderThanTheAccessToken = true; - } catch (err) { - /** - * if error type is not TRY_REFRESH_TOKEN, we return the - * error to the user - */ - if (err.type !== STError.TRY_REFRESH_TOKEN) { - throw err; - } - /** - * if it comes here, it means token verification has failed. - * It may be due to: - * - signing key was updated and this token was signed with new key - * - access token is actually expired - * - access token was signed with the older signing key - * - * if access token is actually expired, we don't need to call core and - * just return TRY_REFRESH_TOKEN to the client - * - * if access token creation time is after this signing key was created - * we need to call core as there are chances that the token - * was signed with the updated signing key - * - * if access token creation time is before oldest signing key was created, - * so if foundASigningKeyThatIsOlderThanTheAccessToken is still false after - * the loop we just return TRY_REFRESH_TOKEN - */ - let payload = parsedAccessToken.payload; - - const timeCreated = sanitizeNumberInput(payload.timeCreated); - const expiryTime = sanitizeNumberInput(payload.expiryTime); - - if (expiryTime === undefined || expiryTime < Date.now()) { - throw err; - } - - if (timeCreated === undefined) { - throw err; - } - - // If we reached a key older than the token and failed to validate the token, - // that means it was signed by a key newer than the cached list. - // In this case we go to the server. - if (timeCreated >= key.createdAt) { - foundASigningKeyThatIsOlderThanTheAccessToken = true; - break; - } - } - } - - // If the token was created before the oldest key in the cache but hasn't expired, then a config value must've changed. - // E.g., the access_token_signing_key_update_interval was reduced, or access_token_signing_key_dynamic was turned on. - // Either way, the user needs to refresh the access token as validating by the server is likely to do nothing. - if (!foundASigningKeyThatIsOlderThanTheAccessToken) { - throw new STError({ - message: "Access token has expired. Please call the refresh API", - type: STError.TRY_REFRESH_TOKEN, - }); - } - - /** - * anti-csrf check if accesstokenInfo is not undefined, - * which means token verification was successful - */ - if (doAntiCsrfCheck) { - if (handShakeInfo.antiCsrf === "VIA_TOKEN") { - if (accessTokenInfo !== undefined) { - if (antiCsrfToken === undefined || antiCsrfToken !== accessTokenInfo.antiCsrfToken) { - if (antiCsrfToken === undefined) { - logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" - ); - throw new STError({ - message: - "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API", - type: STError.TRY_REFRESH_TOKEN, - }); - } else { - logDebugMessage( - "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" - ); - throw new STError({ - message: "anti-csrf check failed", - type: STError.TRY_REFRESH_TOKEN, - }); - } - } - } - } else if (handShakeInfo.antiCsrf === "VIA_CUSTOM_HEADER") { - if (!containsCustomHeader) { - logDebugMessage("getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed"); - throw new STError({ - message: - "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request, or set doAntiCsrfCheck to false for this API", - type: STError.TRY_REFRESH_TOKEN, - }); - } - } - } - if ( - accessTokenInfo !== undefined && - !handShakeInfo.accessTokenBlacklistingEnabled && - accessTokenInfo.parentRefreshTokenHash1 === undefined - ) { - return { - session: { - handle: accessTokenInfo.sessionHandle, - userId: accessTokenInfo.userId, - userDataInJWT: accessTokenInfo.userData, - }, - }; - } - - ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - - let requestBody: { - accessToken: string; - antiCsrfToken?: string; - doAntiCsrfCheck: boolean; - enableAntiCsrf?: boolean; - } = { - accessToken: parsedAccessToken.rawTokenString, - antiCsrfToken, - doAntiCsrfCheck, - enableAntiCsrf: handShakeInfo.antiCsrf === "VIA_TOKEN", - }; - - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/verify"), requestBody); - if (response.status === "OK") { - helpers.updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - delete response.status; - delete response.jwtSigningPublicKey; - delete response.jwtSigningPublicKeyExpiryTime; - delete response.jwtSigningPublicKeyList; - return response; - } else if (response.status === "UNAUTHORISED") { - logDebugMessage("getSession: Returning UNAUTHORISED because of core response"); - throw new STError({ - message: response.message, - type: STError.UNAUTHORISED, - }); - } else { - if ( - response.jwtSigningPublicKeyList !== undefined || - (response.jwtSigningPublicKey !== undefined && response.jwtSigningPublicKeyExpiryTime !== undefined) - ) { - // after CDI 2.7.1, the API returns the new keys - helpers.updateJwtSigningPublicKeyInfo( - response.jwtSigningPublicKeyList, - response.jwtSigningPublicKey, - response.jwtSigningPublicKeyExpiryTime - ); - } else { - // we force update the signing keys... - await helpers.getHandshakeInfo(true); - } - logDebugMessage("getSession: Returning TRY_REFRESH_TOKEN because of core response."); - throw new STError({ - message: response.message, - type: STError.TRY_REFRESH_TOKEN, - }); - } -} - -/** - * @description Retrieves session information from storage for a given session handle - * @returns session data stored in the database, including userData and access token payload, or undefined if sessionHandle is invalid - */ -export async function getSessionInformation( - helpers: Helpers, - sessionHandle: string -): Promise { - let apiVersion = await helpers.querier.getAPIVersion(); - - if (maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error("Please use core version >= 3.5 to call this function."); - } - - let response = await helpers.querier.sendGetRequest(new NormalisedURLPath("/recipe/session"), { - sessionHandle, - }); - - if (response.status === "OK") { - // Change keys to make them more readable - response["sessionData"] = response.userDataInDatabase; - response["accessTokenPayload"] = response.userDataInJWT; - - delete response.userDataInJWT; - delete response.userDataInJWT; - - return response; - } else { - return undefined; - } -} - -/** - * @description generates new access and refresh tokens for a given refresh token. Called when client's access token has expired. - * @sideEffects calls onTokenTheftDetection if token theft is detected. - */ -export async function refreshSession( - helpers: Helpers, - refreshToken: string, - antiCsrfToken: string | undefined, - containsCustomHeader: boolean, - transferMethod: TokenTransferMethod -): Promise { - let handShakeInfo = await helpers.getHandshakeInfo(); - - let requestBody: { - refreshToken: string; - antiCsrfToken?: string; - enableAntiCsrf?: boolean; - } = { - refreshToken, - antiCsrfToken, - enableAntiCsrf: transferMethod === "cookie" && handShakeInfo.antiCsrf === "VIA_TOKEN", - }; - - if (handShakeInfo.antiCsrf === "VIA_CUSTOM_HEADER" && transferMethod === "cookie") { - if (!containsCustomHeader) { - logDebugMessage("refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed"); - throw new STError({ - message: "anti-csrf check failed. Please pass 'rid: \"session\"' header in the request.", - type: STError.UNAUTHORISED, - payload: { - clearTokens: false, // see https://github.com/supertokens/supertokens-node/issues/141 - }, - }); - } - } - - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/refresh"), requestBody); - if (response.status === "OK") { - delete response.status; - return response; - } else if (response.status === "UNAUTHORISED") { - logDebugMessage("refreshSession: Returning UNAUTHORISED because of core response"); - throw new STError({ - message: response.message, - type: STError.UNAUTHORISED, - }); - } else { - logDebugMessage("refreshSession: Returning TOKEN_THEFT_DETECTED because of core response"); - throw new STError({ - message: "Token theft detected", - payload: { - userId: response.session.userId, - sessionHandle: response.session.handle, - }, - type: STError.TOKEN_THEFT_DETECTED, - }); - } -} - -/** - * @description deletes session info of a user from db. This only invalidates the refresh token. Not the access token. - * Access tokens cannot be immediately invalidated. Unless we add a blacklisting method. Or changed the private key to sign them. - */ -export async function revokeAllSessionsForUser(helpers: Helpers, userId: string): Promise { - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/remove"), { - userId, - }); - return response.sessionHandlesRevoked; -} - -/** - * @description gets all session handles for current user. Please do not call this unless this user is authenticated. - */ -export async function getAllSessionHandlesForUser(helpers: Helpers, userId: string): Promise { - let response = await helpers.querier.sendGetRequest(new NormalisedURLPath("/recipe/session/user"), { - userId, - }); - return response.sessionHandles; -} - -/** - * @description call to destroy one session - * @returns true if session was deleted from db. Else false in case there was nothing to delete - */ -export async function revokeSession(helpers: Helpers, sessionHandle: string): Promise { - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/remove"), { - sessionHandles: [sessionHandle], - }); - return response.sessionHandlesRevoked.length === 1; -} - -/** - * @description call to destroy multiple sessions - * @returns list of sessions revoked - */ -export async function revokeMultipleSessions(helpers: Helpers, sessionHandles: string[]): Promise { - let response = await helpers.querier.sendPostRequest(new NormalisedURLPath("/recipe/session/remove"), { - sessionHandles, - }); - return response.sessionHandlesRevoked; -} - -/** - * @description: It provides no locking mechanism in case other processes are updating session data for this session as well. - */ -export async function updateSessionData( - helpers: Helpers, - sessionHandle: string, - newSessionData: any -): Promise { - newSessionData = newSessionData === null || newSessionData === undefined ? {} : newSessionData; - let response = await helpers.querier.sendPutRequest(new NormalisedURLPath("/recipe/session/data"), { - sessionHandle, - userDataInDatabase: newSessionData, - }); - if (response.status === "UNAUTHORISED") { - return false; - } - return true; -} - -export async function updateAccessTokenPayload( - helpers: Helpers, - sessionHandle: string, - newAccessTokenPayload: any -): Promise { - newAccessTokenPayload = - newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - let response = await helpers.querier.sendPutRequest(new NormalisedURLPath("/recipe/jwt/data"), { - sessionHandle, - userDataInJWT: newAccessTokenPayload, - }); - if (response.status === "UNAUTHORISED") { - return false; - } - return true; -} diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts deleted file mode 100644 index 5551c44e8..000000000 --- a/lib/ts/recipe/session/types.ts +++ /dev/null @@ -1,505 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { BaseRequest, BaseResponse } from "../../framework"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; -import OverrideableBuilder from "supertokens-js-override"; -import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; -import { JSONObject, JSONValue } from "../../types"; -import { GeneralErrorResponse } from "../../types"; - -export type KeyInfo = { - publicKey: string; - expiryTime: number; - createdAt: number; -}; - -export type AntiCsrfType = "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE"; -export type StoredHandshakeInfo = { - antiCsrf: AntiCsrfType; - accessTokenBlacklistingEnabled: boolean; - accessTokenValidity: number; - refreshTokenValidity: number; -} & ( - | { - // Stored after 2.9 - jwtSigningPublicKeyList: KeyInfo[]; - } - | { - // Stored before 2.9 - jwtSigningPublicKeyList: undefined; - jwtSigningPublicKey: string; - jwtSigningPublicKeyExpiryTime: number; - } -); - -export type CreateOrRefreshAPIResponse = { - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken: { - token: string; - expiry: number; - createdTime: number; - }; - refreshToken: { - token: string; - expiry: number; - createdTime: number; - }; - antiCsrfToken: string | undefined; -}; - -export interface ErrorHandlers { - onUnauthorised?: ErrorHandlerMiddleware; - onTokenTheftDetected?: TokenTheftErrorHandlerMiddleware; - onInvalidClaim?: InvalidClaimErrorHandlerMiddleware; -} - -export type TokenType = "access" | "refresh"; - -// When adding a new token transfer method, it's also necessary to update the related constant (availableTokenTransferMethods) -export type TokenTransferMethod = "header" | "cookie"; - -export type TypeInput = { - sessionExpiredStatusCode?: number; - invalidClaimStatusCode?: number; - accessTokenPath?: string; - cookieSecure?: boolean; - cookieSameSite?: "strict" | "lax" | "none"; - cookieDomain?: string; - - getTokenTransferMethod?: (input: { - req: BaseRequest; - forCreateNewSession: boolean; - userContext: any; - }) => TokenTransferMethod | "any"; - - errorHandlers?: ErrorHandlers; - antiCsrf?: "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE"; - jwt?: - | { - enable: true; - propertyNameInAccessTokenPayload?: string; - issuer?: string; - } - | { enable: false }; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; - }; -}; - -export type TypeNormalisedInput = { - refreshTokenPath: NormalisedURLPath; - accessTokenPath: NormalisedURLPath; - cookieDomain: string | undefined; - cookieSameSite: "strict" | "lax" | "none"; - cookieSecure: boolean; - sessionExpiredStatusCode: number; - errorHandlers: NormalisedErrorHandlers; - antiCsrf: "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE"; - - getTokenTransferMethod: (input: { - req: BaseRequest; - forCreateNewSession: boolean; - userContext: any; - }) => TokenTransferMethod | "any"; - - invalidClaimStatusCode: number; - jwt: { - enable: boolean; - propertyNameInAccessTokenPayload: string; - issuer?: string; - }; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - openIdFeature?: { - functions?: ( - originalImplementation: OpenIdRecipeInterface, - builder?: OverrideableBuilder - ) => OpenIdRecipeInterface; - apis?: ( - originalImplementation: OpenIdAPIInterface, - builder?: OverrideableBuilder - ) => OpenIdAPIInterface; - jwtFeature?: { - functions?: ( - originalImplementation: JWTRecipeInterface, - builder?: OverrideableBuilder - ) => JWTRecipeInterface; - apis?: ( - originalImplementation: JWTAPIInterface, - builder?: OverrideableBuilder - ) => JWTAPIInterface; - }; - }; - }; -}; - -export interface SessionRequest extends BaseRequest { - session?: SessionContainerInterface; -} - -export interface ErrorHandlerMiddleware { - (message: string, request: BaseRequest, response: BaseResponse): Promise; -} - -export interface TokenTheftErrorHandlerMiddleware { - (sessionHandle: string, userId: string, request: BaseRequest, response: BaseResponse): Promise; -} - -export interface InvalidClaimErrorHandlerMiddleware { - (validatorErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse): Promise; -} - -export interface NormalisedErrorHandlers { - onUnauthorised: ErrorHandlerMiddleware; - onTryRefreshToken: ErrorHandlerMiddleware; - onTokenTheftDetected: TokenTheftErrorHandlerMiddleware; - onInvalidClaim: InvalidClaimErrorHandlerMiddleware; -} - -export interface VerifySessionOptions { - antiCsrfCheck?: boolean; - sessionRequired?: boolean; - overrideGlobalClaimValidators?: ( - globalClaimValidators: SessionClaimValidator[], - session: SessionContainerInterface, - userContext: any - ) => Promise | SessionClaimValidator[]; -} - -export type RecipeInterface = { - createNewSession(input: { - req: BaseRequest; - res: BaseResponse; - userId: string; - accessTokenPayload?: any; - sessionData?: any; - userContext: any; - }): Promise; - - getGlobalClaimValidators(input: { - userId: string; - claimValidatorsAddedByOtherRecipes: SessionClaimValidator[]; - userContext: any; - }): Promise | SessionClaimValidator[]; - - getSession(input: { - req: BaseRequest; - res: BaseResponse; - options?: VerifySessionOptions; - userContext: any; - }): Promise; - - refreshSession(input: { - req: BaseRequest; - res: BaseResponse; - userContext: any; - }): Promise; - /** - * Used to retrieve all session information for a given session handle. Can be used in place of: - * - getSessionData - * - getAccessTokenPayload - * - * Returns undefined if the sessionHandle does not exist - */ - getSessionInformation(input: { sessionHandle: string; userContext: any }): Promise; - - revokeAllSessionsForUser(input: { userId: string; userContext: any }): Promise; - - getAllSessionHandlesForUser(input: { userId: string; userContext: any }): Promise; - - revokeSession(input: { sessionHandle: string; userContext: any }): Promise; - - revokeMultipleSessions(input: { sessionHandles: string[]; userContext: any }): Promise; - - // Returns false if the sessionHandle does not exist - updateSessionData(input: { sessionHandle: string; newSessionData: any; userContext: any }): Promise; - - /** - * @deprecated Use mergeIntoAccessTokenPayload instead - * @returns {Promise} Returns false if the sessionHandle does not exist - */ - updateAccessTokenPayload(input: { - sessionHandle: string; - newAccessTokenPayload: any; - userContext: any; - }): Promise; - - mergeIntoAccessTokenPayload(input: { - sessionHandle: string; - accessTokenPayloadUpdate: JSONObject; - userContext: any; - }): Promise; - - /** - * @returns {Promise} Returns false if the sessionHandle does not exist - */ - regenerateAccessToken(input: { - accessToken: string; - newAccessTokenPayload?: any; - userContext: any; - }): Promise< - | { - status: "OK"; - session: { - handle: string; - userId: string; - userDataInJWT: any; - }; - accessToken?: { - token: string; - expiry: number; - createdTime: number; - }; - } - | undefined - >; - - getAccessTokenLifeTimeMS(input: { userContext: any }): Promise; - - getRefreshTokenLifeTimeMS(input: { userContext: any }): Promise; - - validateClaims(input: { - userId: string; - accessTokenPayload: any; - claimValidators: SessionClaimValidator[]; - userContext: any; - }): Promise<{ - invalidClaims: ClaimValidationError[]; - accessTokenPayloadUpdate?: any; - }>; - - validateClaimsInJWTPayload(input: { - userId: string; - jwtPayload: JSONObject; - claimValidators: SessionClaimValidator[]; - userContext: any; - }): Promise<{ - status: "OK"; - invalidClaims: ClaimValidationError[]; - }>; - - fetchAndSetClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; - setClaimValue(input: { - sessionHandle: string; - claim: SessionClaim; - value: T; - userContext: any; - }): Promise; - - getClaimValue(input: { - sessionHandle: string; - claim: SessionClaim; - userContext: any; - }): Promise< - | { - status: "SESSION_DOES_NOT_EXIST_ERROR"; - } - | { - status: "OK"; - value: T | undefined; - } - >; - - removeClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; -}; - -export interface SessionContainerInterface { - revokeSession(userContext?: any): Promise; - - getSessionData(userContext?: any): Promise; - - updateSessionData(newSessionData: any, userContext?: any): Promise; - - getUserId(userContext?: any): string; - - getAccessTokenPayload(userContext?: any): any; - - getHandle(userContext?: any): string; - - getAccessToken(userContext?: any): string; - - /** - * @deprecated Use mergeIntoAccessTokenPayload instead - */ - updateAccessTokenPayload(newAccessTokenPayload: any, userContext?: any): Promise; - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: any): Promise; - - getTimeCreated(userContext?: any): Promise; - - getExpiry(userContext?: any): Promise; - - assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise; - fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise; - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise; - getClaimValue(claim: SessionClaim, userContext?: any): Promise; - removeClaim(claim: SessionClaim, userContext?: any): Promise; -} - -export type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - req: BaseRequest; - res: BaseResponse; -}; - -export type APIInterface = { - /** - * We do not add a GeneralErrorResponse response to this API - * since it's not something that is directly called by the user on the - * frontend anyway - */ - refreshPOST: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); - - signOutPOST: - | undefined - | ((input: { - options: APIOptions; - // the reason we make this optional is cause it allows users to do something in - // case a session does not exist and the sign out button is pressed. It is - // rare that something needs to be done in this case, but making it like this - // has little disadvantages. - session: SessionContainerInterface | undefined; - userContext: any; - }) => Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - >); - - verifySession(input: { - verifySessionOptions: VerifySessionOptions | undefined; - options: APIOptions; - userContext: any; - }): Promise; -}; - -export type SessionInformation = { - sessionHandle: string; - userId: string; - sessionData: any; - expiry: number; - accessTokenPayload: any; - timeCreated: number; -}; - -export type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: JSONValue }; -export type ClaimValidationError = { - id: string; - reason?: JSONValue; -}; - -export type SessionClaimValidator = ( - | // We split the type like this to express that either both claim and shouldRefetch is defined or neither. - { - claim: SessionClaim; - /** - * Decides if we need to refetch the claim value before checking the payload with `isValid`. - * E.g.: if the information in the payload is expired, or is not sufficient for this check. - */ - shouldRefetch: (payload: any, userContext: any) => Promise | boolean; - } - | {} -) & { - id: string; - /** - * Decides if the claim is valid based on the payload (and not checking DB or anything else) - */ - validate: (payload: any, userContext: any) => Promise; -}; - -export abstract class SessionClaim { - constructor(public readonly key: string) {} - - /** - * This methods fetches the current value of this claim for the user. - * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database - * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. - */ - abstract fetchValue(userId: string, userContext: any): Promise | T | undefined; - - /** - * Saves the provided value into the payload, by cloning and updating the entire object. - * - * @returns The modified payload object - */ - abstract addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; - - /** - * Removes the claim from the payload by setting it to null, so mergeIntoAccessTokenPayload clears it - * - * @returns The modified payload object - */ - abstract removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; - - /** - * Removes the claim from the payload, by cloning and updating the entire object. - * - * @returns The modified payload object - */ - abstract removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; - - /** - * Gets the value of the claim stored in the payload - * - * @returns Claim value - */ - abstract getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; - - async build(userId: string, userContext?: any): Promise { - const value = await this.fetchValue(userId, userContext); - - if (value === undefined) { - return {}; - } - - return this.addToPayload_internal({}, value, userContext); - } -} diff --git a/lib/ts/recipe/session/utils.ts b/lib/ts/recipe/session/utils.ts deleted file mode 100644 index 33daf4290..000000000 --- a/lib/ts/recipe/session/utils.ts +++ /dev/null @@ -1,353 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { - CreateOrRefreshAPIResponse, - TypeInput, - TypeNormalisedInput, - NormalisedErrorHandlers, - ClaimValidationError, - SessionClaimValidator, - SessionContainerInterface, - VerifySessionOptions, - TokenTransferMethod, -} from "./types"; -import { setFrontTokenInHeaders, setAntiCsrfTokenInHeaders, setToken, getAuthModeFromHeader } from "./cookieAndHeaders"; -import { URL } from "url"; -import SessionRecipe from "./recipe"; -import { REFRESH_API_PATH } from "./constants"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { NormalisedAppinfo } from "../../types"; -import { isAnIpAddress } from "../../utils"; -import { RecipeInterface, APIInterface } from "./types"; -import { BaseRequest, BaseResponse } from "../../framework"; -import { sendNon200ResponseWithMessage, sendNon200Response } from "../../utils"; -import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY, JWT_RESERVED_KEY_USE_ERROR_MESSAGE } from "./with-jwt/constants"; -import { logDebugMessage } from "../../logger"; - -export async function sendTryRefreshTokenResponse( - recipeInstance: SessionRecipe, - _: string, - __: BaseRequest, - response: BaseResponse -) { - sendNon200ResponseWithMessage(response, "try refresh token", recipeInstance.config.sessionExpiredStatusCode); -} - -export async function sendUnauthorisedResponse( - recipeInstance: SessionRecipe, - _: string, - __: BaseRequest, - response: BaseResponse -) { - sendNon200ResponseWithMessage(response, "unauthorised", recipeInstance.config.sessionExpiredStatusCode); -} - -export async function sendInvalidClaimResponse( - recipeInstance: SessionRecipe, - claimValidationErrors: ClaimValidationError[], - __: BaseRequest, - response: BaseResponse -) { - sendNon200Response(response, recipeInstance.config.invalidClaimStatusCode, { - message: "invalid claim", - claimValidationErrors, - }); -} - -export async function sendTokenTheftDetectedResponse( - recipeInstance: SessionRecipe, - sessionHandle: string, - _: string, - __: BaseRequest, - response: BaseResponse -) { - await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }); - sendNon200ResponseWithMessage(response, "token theft detected", recipeInstance.config.sessionExpiredStatusCode); -} - -export function normaliseSessionScopeOrThrowError(sessionScope: string): string { - function helper(sessionScope: string): string { - sessionScope = sessionScope.trim().toLowerCase(); - - // first we convert it to a URL so that we can use the URL class - if (sessionScope.startsWith(".")) { - sessionScope = sessionScope.substr(1); - } - - if (!sessionScope.startsWith("http://") && !sessionScope.startsWith("https://")) { - sessionScope = "http://" + sessionScope; - } - - try { - let urlObj = new URL(sessionScope); - sessionScope = urlObj.hostname; - - // remove leading dot - if (sessionScope.startsWith(".")) { - sessionScope = sessionScope.substr(1); - } - - return sessionScope; - } catch (err) { - throw new Error("Please provide a valid sessionScope"); - } - } - - let noDotNormalised = helper(sessionScope); - - if (noDotNormalised === "localhost" || isAnIpAddress(noDotNormalised)) { - return noDotNormalised; - } - - if (sessionScope.startsWith(".")) { - return "." + noDotNormalised; - } - - return noDotNormalised; -} - -export function getURLProtocol(url: string): string { - let urlObj = new URL(url); - return urlObj.protocol; -} - -export function validateAndNormaliseUserInput( - recipeInstance: SessionRecipe, - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput { - let cookieDomain = - config === undefined || config.cookieDomain === undefined - ? undefined - : normaliseSessionScopeOrThrowError(config.cookieDomain); - let accessTokenPath = - config === undefined || config.accessTokenPath === undefined - ? new NormalisedURLPath("/") - : new NormalisedURLPath(config.accessTokenPath); - let protocolOfAPIDomain = getURLProtocol(appInfo.apiDomain.getAsStringDangerous()); - let protocolOfWebsiteDomain = getURLProtocol(appInfo.websiteDomain.getAsStringDangerous()); - - let cookieSameSite: "strict" | "lax" | "none" = - appInfo.topLevelAPIDomain !== appInfo.topLevelWebsiteDomain || protocolOfAPIDomain !== protocolOfWebsiteDomain - ? "none" - : "lax"; - cookieSameSite = - config === undefined || config.cookieSameSite === undefined - ? cookieSameSite - : normaliseSameSiteOrThrowError(config.cookieSameSite); - - let cookieSecure = - config === undefined || config.cookieSecure === undefined - ? appInfo.apiDomain.getAsStringDangerous().startsWith("https") - : config.cookieSecure; - - let sessionExpiredStatusCode = - config === undefined || config.sessionExpiredStatusCode === undefined ? 401 : config.sessionExpiredStatusCode; - const invalidClaimStatusCode = config?.invalidClaimStatusCode ?? 403; - - if (sessionExpiredStatusCode === invalidClaimStatusCode) { - throw new Error("sessionExpiredStatusCode and sessionExpiredStatusCode must be different"); - } - - if (config !== undefined && config.antiCsrf !== undefined) { - if (config.antiCsrf !== "NONE" && config.antiCsrf !== "VIA_CUSTOM_HEADER" && config.antiCsrf !== "VIA_TOKEN") { - throw new Error("antiCsrf config must be one of 'NONE' or 'VIA_CUSTOM_HEADER' or 'VIA_TOKEN'"); - } - } - - let antiCsrf: "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE" = - config === undefined || config.antiCsrf === undefined - ? cookieSameSite === "none" - ? "VIA_CUSTOM_HEADER" - : "NONE" - : config.antiCsrf; - - let errorHandlers: NormalisedErrorHandlers = { - onTokenTheftDetected: async ( - sessionHandle: string, - userId: string, - request: BaseRequest, - response: BaseResponse - ) => { - return await sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, userId, request, response); - }, - onTryRefreshToken: async (message: string, request: BaseRequest, response: BaseResponse) => { - return await sendTryRefreshTokenResponse(recipeInstance, message, request, response); - }, - onUnauthorised: async (message: string, request: BaseRequest, response: BaseResponse) => { - return await sendUnauthorisedResponse(recipeInstance, message, request, response); - }, - onInvalidClaim: (validationErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse) => { - return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response); - }, - }; - if (config !== undefined && config.errorHandlers !== undefined) { - if (config.errorHandlers.onTokenTheftDetected !== undefined) { - errorHandlers.onTokenTheftDetected = config.errorHandlers.onTokenTheftDetected; - } - if (config.errorHandlers.onUnauthorised !== undefined) { - errorHandlers.onUnauthorised = config.errorHandlers.onUnauthorised; - } - if (config.errorHandlers.onInvalidClaim !== undefined) { - errorHandlers.onInvalidClaim = config.errorHandlers.onInvalidClaim; - } - } - - let enableJWT = false; - let accessTokenPayloadJWTPropertyName = "jwt"; - let issuer: string | undefined; - - if (config !== undefined && config.jwt !== undefined && config.jwt.enable === true) { - enableJWT = true; - let jwtPropertyName = config.jwt.propertyNameInAccessTokenPayload; - issuer = config.jwt.issuer; - - if (jwtPropertyName !== undefined) { - if (jwtPropertyName === ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY) { - throw new Error(JWT_RESERVED_KEY_USE_ERROR_MESSAGE); - } - - accessTokenPayloadJWTPropertyName = jwtPropertyName; - } - } - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - return { - refreshTokenPath: appInfo.apiBasePath.appendPath(new NormalisedURLPath(REFRESH_API_PATH)), - accessTokenPath, - getTokenTransferMethod: - config?.getTokenTransferMethod === undefined - ? defaultGetTokenTransferMethod - : config.getTokenTransferMethod, - cookieDomain, - cookieSameSite, - cookieSecure, - sessionExpiredStatusCode, - errorHandlers, - antiCsrf, - override, - invalidClaimStatusCode, - jwt: { - enable: enableJWT, - propertyNameInAccessTokenPayload: accessTokenPayloadJWTPropertyName, - issuer, - }, - }; -} - -export function normaliseSameSiteOrThrowError(sameSite: string): "strict" | "lax" | "none" { - sameSite = sameSite.trim(); - sameSite = sameSite.toLocaleLowerCase(); - if (sameSite !== "strict" && sameSite !== "lax" && sameSite !== "none") { - throw new Error(`cookie same site must be one of "strict", "lax", or "none"`); - } - return sameSite; -} - -export function attachTokensToResponse( - config: TypeNormalisedInput, - res: BaseResponse, - response: CreateOrRefreshAPIResponse, - transferMethod: TokenTransferMethod -) { - let accessToken = response.accessToken; - let refreshToken = response.refreshToken; - setFrontTokenInHeaders(res, response.session.userId, response.accessToken.expiry, response.session.userDataInJWT); - setToken( - config, - res, - "access", - accessToken.token, - // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. - // Even if the token is expired the presence of the token indicates that the user could have a valid refresh - // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. - Date.now() + 3153600000000, - transferMethod - ); - setToken(config, res, "refresh", refreshToken.token, refreshToken.expiry, transferMethod); - if (response.antiCsrfToken !== undefined) { - setAntiCsrfTokenInHeaders(res, response.antiCsrfToken); - } -} - -export async function getRequiredClaimValidators( - session: SessionContainerInterface, - overrideGlobalClaimValidators: VerifySessionOptions["overrideGlobalClaimValidators"], - userContext: any -) { - const claimValidatorsAddedByOtherRecipes = SessionRecipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes(); - const globalClaimValidators: SessionClaimValidator[] = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getGlobalClaimValidators( - { - userId: session.getUserId(), - claimValidatorsAddedByOtherRecipes, - userContext, - } - ); - - return overrideGlobalClaimValidators !== undefined - ? await overrideGlobalClaimValidators(globalClaimValidators, session, userContext) - : globalClaimValidators; -} - -export async function validateClaimsInPayload( - claimValidators: SessionClaimValidator[], - newAccessTokenPayload: any, - userContext: any -) { - const validationErrors = []; - for (const validator of claimValidators) { - const claimValidationResult = await validator.validate(newAccessTokenPayload, userContext); - logDebugMessage( - "validateClaimsInPayload " + validator.id + " validation res " + JSON.stringify(claimValidationResult) - ); - if (!claimValidationResult.isValid) { - validationErrors.push({ - id: validator.id, - reason: claimValidationResult.reason, - }); - } - } - return validationErrors; -} - -function defaultGetTokenTransferMethod({ - req, - forCreateNewSession, -}: { - req: BaseRequest; - forCreateNewSession: boolean; -}): TokenTransferMethod | "any" { - // We allow fallback (checking headers then cookies) by default when validating - if (!forCreateNewSession) { - return "any"; - } - - // In create new session we respect the frontend preference by default - switch (getAuthModeFromHeader(req)) { - case "header": - return "header"; - case "cookie": - return "cookie"; - default: - return "any"; - } -} diff --git a/lib/ts/recipe/session/with-jwt/constants.ts b/lib/ts/recipe/session/with-jwt/constants.ts deleted file mode 100644 index 1ec1b8482..000000000 --- a/lib/ts/recipe/session/with-jwt/constants.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -/* - This key is used to determine the property name used when adding the jwt to the access token payload - For example if the Session recipe is initialised with config - { - ... - jwt: { - enable: true, - propertyNameInAccessTokenPayload: "jwtKey", - }, - ... - } - - The access token payload after creating a session would look like - { - ... - jwtKey: "JWT_STRING", - _jwtPName: "jwtKey", - } - - When trying to refresh the session or updating the access token payload, this key is used to determine and retrieve - the exsiting JWT from the access token payload. -*/ -export const ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY = "_jwtPName"; -export const JWT_RESERVED_KEY_USE_ERROR_MESSAGE = `${ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY} is a reserved property name, please use a different key name for the jwt`; diff --git a/lib/ts/recipe/session/with-jwt/index.ts b/lib/ts/recipe/session/with-jwt/index.ts deleted file mode 100644 index ac3dfdd9e..000000000 --- a/lib/ts/recipe/session/with-jwt/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import OriginalImplementation from "./recipeImplementation"; - -export default OriginalImplementation; diff --git a/lib/ts/recipe/session/with-jwt/recipeImplementation.ts b/lib/ts/recipe/session/with-jwt/recipeImplementation.ts deleted file mode 100644 index 9c93652fb..000000000 --- a/lib/ts/recipe/session/with-jwt/recipeImplementation.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { decode } from "jsonwebtoken"; - -import { RecipeInterface } from "../"; -import { RecipeInterface as OpenIdRecipeInterface } from "../../openid/types"; -import { SessionContainerInterface, SessionInformation, TypeNormalisedInput, VerifySessionOptions } from "../types"; -import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY } from "./constants"; -import SessionClassWithJWT from "./sessionClass"; -import * as assert from "assert"; -import { addJWTToAccessTokenPayload } from "./utils"; -import { JSONObject } from "../../../types"; -import { BaseResponse } from "../../../framework/response"; -import { BaseRequest } from "../../../framework/request"; - -// Time difference between JWT expiry and access token expiry (JWT expiry = access token expiry + EXPIRY_OFFSET_SECONDS) -let EXPIRY_OFFSET_SECONDS = 30; - -// This function should only be used during testing -export function setJWTExpiryOffsetSecondsForTesting(offset: number) { - if (process.env.TEST_MODE !== "testing") { - throw Error("calling testing function in non testing env"); - } - EXPIRY_OFFSET_SECONDS = offset; -} - -export default function ( - originalImplementation: RecipeInterface, - openIdRecipeImplementation: OpenIdRecipeInterface, - config: TypeNormalisedInput -): RecipeInterface { - function getJWTExpiry(accessTokenExpiry: number): number { - return accessTokenExpiry + EXPIRY_OFFSET_SECONDS; - } - - async function jwtAwareUpdateAccessTokenPayload( - sessionInformation: SessionInformation, - newAccessTokenPayload: any, - userContext: any - ) { - let accessTokenPayload = sessionInformation.accessTokenPayload; - - let existingJwtPropertyName = accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - - if (existingJwtPropertyName === undefined) { - return await originalImplementation.updateAccessTokenPayload({ - sessionHandle: sessionInformation.sessionHandle, - newAccessTokenPayload, - userContext, - }); - } - - let existingJwt = accessTokenPayload[existingJwtPropertyName]; - assert.notStrictEqual(existingJwt, undefined); - - let currentTimeInSeconds = Date.now() / 1000; - let decodedPayload = decode(existingJwt, { json: true }); - - // decode possibly returns null - if (decodedPayload === null || decodedPayload.exp === undefined) { - throw new Error("Error reading JWT from session"); - } - - let jwtExpiry = decodedPayload.exp - currentTimeInSeconds; - - if (jwtExpiry <= 0) { - // it can come here if someone calls this function well after - // the access token and the jwt payload have expired. In this case, - // we still want the jwt payload to update, but the resulting JWT should - // not be alive for too long (since it's expired already). So we set it to - // 1 second lifetime. - jwtExpiry = 1; - } - - newAccessTokenPayload = await addJWTToAccessTokenPayload({ - accessTokenPayload: newAccessTokenPayload, - jwtExpiry, - userId: sessionInformation.userId, - jwtPropertyName: existingJwtPropertyName, - openIdRecipeImplementation, - userContext, - }); - - return await originalImplementation.updateAccessTokenPayload({ - sessionHandle: sessionInformation.sessionHandle, - newAccessTokenPayload, - userContext, - }); - } - - return { - ...originalImplementation, - createNewSession: async function ( - this: RecipeInterface, - { - req, - res, - userId, - accessTokenPayload, - sessionData, - userContext, - }: { - req: BaseRequest; - res: BaseResponse; - userId: string; - accessTokenPayload?: any; - sessionData?: any; - userContext: any; - } - ): Promise { - accessTokenPayload = - accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; - let accessTokenValidityInSeconds = Math.ceil((await this.getAccessTokenLifeTimeMS({ userContext })) / 1000); - - accessTokenPayload = await addJWTToAccessTokenPayload({ - accessTokenPayload, - jwtExpiry: getJWTExpiry(accessTokenValidityInSeconds), - userId, - jwtPropertyName: config.jwt.propertyNameInAccessTokenPayload, - openIdRecipeImplementation, - userContext, - }); - - let sessionContainer = await originalImplementation.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - userContext, - }); - - return new SessionClassWithJWT(sessionContainer, openIdRecipeImplementation); - }, - getSession: async function ( - this: RecipeInterface, - { - req, - res, - options, - userContext, - }: { - req: BaseRequest; - res: BaseResponse; - options?: VerifySessionOptions; - userContext: any; - } - ): Promise { - let sessionContainer = await originalImplementation.getSession({ req, res, options, userContext }); - - if (sessionContainer === undefined) { - return undefined; - } - - return new SessionClassWithJWT(sessionContainer, openIdRecipeImplementation); - }, - refreshSession: async function ( - this: RecipeInterface, - { - req, - res, - userContext, - }: { - req: BaseRequest; - res: BaseResponse; - userContext: any; - } - ): Promise { - let accessTokenValidityInSeconds = Math.ceil((await this.getAccessTokenLifeTimeMS({ userContext })) / 1000); - - // Refresh session first because this will create a new access token - let newSession = await originalImplementation.refreshSession({ req, res, userContext }); - let accessTokenPayload = newSession.getAccessTokenPayload(); - - accessTokenPayload = await addJWTToAccessTokenPayload({ - accessTokenPayload, - jwtExpiry: getJWTExpiry(accessTokenValidityInSeconds), - userId: newSession.getUserId(), - jwtPropertyName: config.jwt.propertyNameInAccessTokenPayload, - openIdRecipeImplementation, - userContext, - }); - - await newSession.updateAccessTokenPayload(accessTokenPayload); - - return new SessionClassWithJWT(newSession, openIdRecipeImplementation); - }, - - mergeIntoAccessTokenPayload: async function ( - this: RecipeInterface, - { - sessionHandle, - accessTokenPayloadUpdate, - userContext, - }: { - sessionHandle: string; - accessTokenPayloadUpdate: JSONObject; - userContext: any; - } - ): Promise { - let sessionInformation = await this.getSessionInformation({ sessionHandle, userContext }); - - if (!sessionInformation) { - return false; - } - - let newAccessTokenPayload = { ...sessionInformation.accessTokenPayload, ...accessTokenPayloadUpdate }; - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete newAccessTokenPayload[key]; - } - } - - return jwtAwareUpdateAccessTokenPayload(sessionInformation, newAccessTokenPayload, userContext); - }, - - /** - * @deprecated use mergeIntoAccessTokenPayload instead - */ - updateAccessTokenPayload: async function ( - this: RecipeInterface, - { - sessionHandle, - newAccessTokenPayload, - userContext, - }: { - sessionHandle: string; - newAccessTokenPayload: any; - userContext: any; - } - ): Promise { - newAccessTokenPayload = - newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - - const sessionInformation = await this.getSessionInformation({ sessionHandle, userContext }); - - if (!sessionInformation) { - return false; - } - - return jwtAwareUpdateAccessTokenPayload(sessionInformation, newAccessTokenPayload, userContext); - }, - }; -} diff --git a/lib/ts/recipe/session/with-jwt/sessionClass.ts b/lib/ts/recipe/session/with-jwt/sessionClass.ts deleted file mode 100644 index 6d11bf8ca..000000000 --- a/lib/ts/recipe/session/with-jwt/sessionClass.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { decode } from "jsonwebtoken"; -import * as assert from "assert"; - -import { RecipeInterface as OpenIdRecipeInterface } from "../../openid/types"; -import { SessionClaim, SessionClaimValidator, SessionContainerInterface } from "../types"; -import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY } from "./constants"; -import { addJWTToAccessTokenPayload } from "./utils"; -import STError from "../error"; -import SessionRecipe from "../recipe"; - -export default class SessionClassWithJWT implements SessionContainerInterface { - constructor( - private readonly originalSessionClass: SessionContainerInterface, - private readonly openIdRecipeImplementation: OpenIdRecipeInterface - ) {} - - revokeSession(userContext?: any): Promise { - return this.originalSessionClass.revokeSession(userContext); - } - getSessionData(userContext?: any): Promise { - return this.originalSessionClass.getSessionData(userContext); - } - updateSessionData(newSessionData: any, userContext?: any): Promise { - return this.originalSessionClass.updateSessionData(newSessionData, userContext); - } - getUserId(userContext?: any): string { - return this.originalSessionClass.getUserId(userContext); - } - getAccessTokenPayload(userContext?: any) { - return this.originalSessionClass.getAccessTokenPayload(userContext); - } - getHandle(userContext?: any): string { - return this.originalSessionClass.getHandle(userContext); - } - getAccessToken(userContext?: any): string { - return this.originalSessionClass.getAccessToken(userContext); - } - getTimeCreated(userContext?: any): Promise { - return this.originalSessionClass.getTimeCreated(userContext); - } - getExpiry(userContext?: any): Promise { - return this.originalSessionClass.getExpiry(userContext); - } - - // We copy the implementation here, since we want to override updateAccessTokenPayload - async assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise { - let validateClaimResponse = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.validateClaims({ - accessTokenPayload: this.getAccessTokenPayload(userContext), - userId: this.getUserId(userContext), - claimValidators, - userContext, - }); - - if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { - await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); - } - - if (validateClaimResponse.invalidClaims.length !== 0) { - throw new STError({ - type: "INVALID_CLAIMS", - message: "INVALID_CLAIMS", - payload: validateClaimResponse.invalidClaims, - }); - } - } - - // We copy the implementation here, since we want to override updateAccessTokenPayload - async fetchAndSetClaim(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any) { - const update = await claim.build(this.getUserId(userContext), userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - - // We copy the implementation here, since we want to override updateAccessTokenPayload - setClaimValue(this: SessionClassWithJWT, claim: SessionClaim, value: T, userContext?: any) { - const update = claim.addToPayload_internal({}, value, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - - // We copy the implementation here, since we want to override updateAccessTokenPayload - async getClaimValue(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any) { - return claim.getValueFromPayload(await this.getAccessTokenPayload(userContext), userContext); - } - - // We copy the implementation here, since we want to override updateAccessTokenPayload - removeClaim(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any) { - const update = claim.removeFromPayloadByMerge_internal({}, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); - } - - // We copy the implementation here, since we want to override updateAccessTokenPayload - async mergeIntoAccessTokenPayload( - this: SessionClassWithJWT, - accessTokenPayloadUpdate: any, - userContext?: any - ): Promise { - const updatedPayload = { ...this.getAccessTokenPayload(userContext), ...accessTokenPayloadUpdate }; - for (const key of Object.keys(accessTokenPayloadUpdate)) { - if (accessTokenPayloadUpdate[key] === null) { - delete updatedPayload[key]; - } - } - - await this.updateAccessTokenPayload(updatedPayload, userContext); - } - - // TODO: figure out a proper way to override just this function - /** - * @deprecated use mergeIntoAccessTokenPayload instead - */ - async updateAccessTokenPayload( - this: SessionClassWithJWT, - newAccessTokenPayload: any | undefined, - userContext?: any - ): Promise { - newAccessTokenPayload = - newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; - let accessTokenPayload = this.getAccessTokenPayload(userContext); - let jwtPropertyName = accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - - if (jwtPropertyName === undefined) { - return this.originalSessionClass.updateAccessTokenPayload(newAccessTokenPayload, userContext); - } - - let existingJWT = accessTokenPayload[jwtPropertyName]; - assert.notStrictEqual(existingJWT, undefined); - - let currentTimeInSeconds = Date.now() / 1000; - let decodedPayload = decode(existingJWT, { json: true }); - - // JsonWebToken.decode possibly returns null - if (decodedPayload === null || decodedPayload.exp === undefined) { - throw new Error("Error reading JWT from session"); - } - - let jwtExpiry = decodedPayload.exp - currentTimeInSeconds; - - if (jwtExpiry <= 0) { - // it can come here if someone calls this function well after - // the access token and the jwt payload have expired (which can happen if an API takes a VERY long time). In this case, we still want the jwt payload to update, but the resulting JWT should - // not be alive for too long (since it's expired already). So we set it to - // 1 second lifetime. - jwtExpiry = 1; - } - - newAccessTokenPayload = await addJWTToAccessTokenPayload({ - accessTokenPayload: newAccessTokenPayload, - jwtExpiry, - userId: this.getUserId(), - jwtPropertyName, - openIdRecipeImplementation: this.openIdRecipeImplementation, - userContext, - }); - - await this.originalSessionClass.updateAccessTokenPayload(newAccessTokenPayload, userContext); - } -} diff --git a/lib/ts/recipe/session/with-jwt/utils.ts b/lib/ts/recipe/session/with-jwt/utils.ts deleted file mode 100644 index f83b49658..000000000 --- a/lib/ts/recipe/session/with-jwt/utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY } from "./constants"; -import { RecipeInterface as OpenIdRecipeInterface } from "../../openid/types"; - -export async function addJWTToAccessTokenPayload({ - accessTokenPayload, - jwtExpiry, - userId, - jwtPropertyName, - openIdRecipeImplementation, - userContext, -}: { - accessTokenPayload: any; - jwtExpiry: number; - userId: string; - jwtPropertyName: string; - openIdRecipeImplementation: OpenIdRecipeInterface; - userContext: any; -}): Promise { - // If jwtPropertyName is not undefined it means that the JWT was added to the access token payload already - let existingJwtPropertyName = accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - - if (existingJwtPropertyName !== undefined) { - // Delete the old JWT and the old property name - delete accessTokenPayload[existingJwtPropertyName]; - delete accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]; - } - - // Create the JWT - let jwtResponse = await openIdRecipeImplementation.createJWT({ - payload: { - /* - We add our claims before the user provided ones so that if they use the same claims - then the final payload will use the values they provide - */ - sub: userId, - ...accessTokenPayload, - }, - validitySeconds: jwtExpiry, - userContext, - }); - - if (jwtResponse.status === "UNSUPPORTED_ALGORITHM_ERROR") { - // Should never come here - throw new Error("JWT Signing algorithm not supported"); - } - - // Add the jwt and the property name to the access token payload - accessTokenPayload = { - ...accessTokenPayload, - /* - We add the JWT after the user defined keys because we want to make sure that it never - gets overwritten by a user defined key. Using the same key as the one configured (or defaulting) - for the JWT should be considered a dev error - - ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY indicates a reserved key used to determine the property name - with which the JWT is set, used to retrieve the JWT from the access token payload during refresg and - updateAccessTokenPayload - - Note: If the user has multiple overrides each with a unique propertyNameInAccessTokenPayload, the logic - for checking the existing JWT when refreshing the session or updating the access token payload will not work. - This is because even though the jwt itself would be created with unique property names, the _jwtPName value would - always be overwritten by the override that runs last and when retrieving the jwt using that key name it cannot be - guaranteed that the right JWT is returned. This case is considered to be a rare requirement and we assume - that users will not need multiple JWT representations of their access token payload. - */ - [jwtPropertyName]: jwtResponse.jwt, - [ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]: jwtPropertyName, - }; - - return accessTokenPayload; -} diff --git a/lib/ts/recipe/thirdparty/api/appleRedirect.ts b/lib/ts/recipe/thirdparty/api/appleRedirect.ts deleted file mode 100644 index b323a1bb5..000000000 --- a/lib/ts/recipe/thirdparty/api/appleRedirect.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { APIInterface, APIOptions } from "../"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function appleRedirectHandler( - apiImplementation: APIInterface, - options: APIOptions -): Promise { - if (apiImplementation.appleRedirectHandlerPOST === undefined) { - return false; - } - - let body = await options.req.getFormData(); - - let state = body.state; - let code = body.code; - - // this will redirect the user... - await apiImplementation.appleRedirectHandlerPOST({ - code, - state, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - return true; -} diff --git a/lib/ts/recipe/thirdparty/api/authorisationUrl.ts b/lib/ts/recipe/thirdparty/api/authorisationUrl.ts deleted file mode 100644 index 035f93d88..000000000 --- a/lib/ts/recipe/thirdparty/api/authorisationUrl.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { send200Response } from "../../../utils"; -import STError from "../error"; -import { APIInterface, APIOptions } from "../"; -import { findRightProvider } from "../utils"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function authorisationUrlAPI( - apiImplementation: APIInterface, - options: APIOptions -): Promise { - if (apiImplementation.authorisationUrlGET === undefined) { - return false; - } - - let thirdPartyId = options.req.getKeyValueFromQuery("thirdPartyId"); - - if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the thirdPartyId as a GET param", - }); - } - - let provider = findRightProvider(options.providers, thirdPartyId, undefined); - if (provider === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "The third party provider " + thirdPartyId + ` seems to be missing from the backend configs.`, - }); - } - - let result = await apiImplementation.authorisationUrlGET({ - provider, - options, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - send200Response(options.res, result); - return true; -} diff --git a/lib/ts/recipe/thirdparty/api/implementation.ts b/lib/ts/recipe/thirdparty/api/implementation.ts deleted file mode 100644 index a3337f6cf..000000000 --- a/lib/ts/recipe/thirdparty/api/implementation.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { APIInterface, APIOptions, User, TypeProvider } from "../"; -import Session from "../../session"; -import { URLSearchParams } from "url"; -import * as axios from "axios"; -import * as qs from "querystring"; -import { SessionContainerInterface } from "../../session/types"; -import { GeneralErrorResponse } from "../../../types"; -import EmailVerification from "../../emailverification/recipe"; - -export default function getAPIInterface(): APIInterface { - return { - authorisationUrlGET: async function ({ - provider, - options, - userContext, - }: { - provider: TypeProvider; - options: APIOptions; - userContext: any; - }): Promise< - | { - status: "OK"; - url: string; - } - | GeneralErrorResponse - > { - let providerInfo = provider.get(undefined, undefined, userContext); - - let params: { [key: string]: string } = {}; - let keys = Object.keys(providerInfo.authorisationRedirect.params); - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - let value = providerInfo.authorisationRedirect.params[key]; - params[key] = typeof value === "function" ? await value(options.req.original) : value; - } - if ( - providerInfo.getRedirectURI !== undefined && - !isUsingDevelopmentClientId(providerInfo.getClientId(userContext)) - ) { - // the backend wants to set the redirectURI - so we set that here. - - // we add the not development keys because the oauth provider will - // redirect to supertokens.io's URL which will redirect the app - // to the the user's website, which will handle the callback as usual. - // If we add this, then instead, the supertokens' site will redirect - // the user to this API layer, which is not needed. - params["redirect_uri"] = providerInfo.getRedirectURI(userContext); - } - - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - params["actual_redirect_uri"] = providerInfo.authorisationRedirect.url; - - Object.keys(params).forEach((key) => { - if (params[key] === providerInfo.getClientId(userContext)) { - params[key] = getActualClientIdFromDevelopmentClientId(providerInfo.getClientId(userContext)); - } - }); - } - - let paramsString = new URLSearchParams(params).toString(); - - let url = `${providerInfo.authorisationRedirect.url}?${paramsString}`; - - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - url = `${DEV_OAUTH_AUTHORIZATION_URL}?${paramsString}`; - } - - return { - status: "OK", - url, - }; - }, - signInUpPOST: async function ({ - provider, - code, - redirectURI, - authCodeResponse, - options, - userContext, - }: { - provider: TypeProvider; - code: string; - redirectURI: string; - authCodeResponse?: any; - clientId?: string; - options: APIOptions; - userContext: any; - }): Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - authCodeResponse: any; - } - | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } - | GeneralErrorResponse - > { - let userInfo; - let accessTokenAPIResponse: any; - - { - let providerInfo = provider.get(undefined, undefined, userContext); - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - redirectURI = DEV_OAUTH_REDIRECT_URL; - } else if (providerInfo.getRedirectURI !== undefined) { - // we overwrite the redirectURI provided by the frontend - // since the backend wants to take charge of setting this. - redirectURI = providerInfo.getRedirectURI(userContext); - } - } - - let providerInfo = provider.get(redirectURI, code, userContext); - - if (authCodeResponse !== undefined) { - accessTokenAPIResponse = { - data: authCodeResponse, - }; - } else { - // we should use code to get the authCodeResponse body - if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { - Object.keys(providerInfo.accessTokenAPI.params).forEach((key) => { - if (providerInfo.accessTokenAPI.params[key] === providerInfo.getClientId(userContext)) { - providerInfo.accessTokenAPI.params[key] = getActualClientIdFromDevelopmentClientId( - providerInfo.getClientId(userContext) - ); - } - }); - } - - accessTokenAPIResponse = await axios.default({ - method: "post", - url: providerInfo.accessTokenAPI.url, - data: qs.stringify(providerInfo.accessTokenAPI.params), - headers: { - "content-type": "application/x-www-form-urlencoded", - accept: "application/json", // few providers like github don't send back json response by default - }, - }); - } - - userInfo = await providerInfo.getProfileInfo(accessTokenAPIResponse.data, userContext); - - let emailInfo = userInfo.email; - if (emailInfo === undefined) { - return { - status: "NO_EMAIL_GIVEN_BY_PROVIDER", - }; - } - let response = await options.recipeImplementation.signInUp({ - thirdPartyId: provider.id, - thirdPartyUserId: userInfo.id, - email: emailInfo.id, - userContext, - }); - - // we set the email as verified if already verified by the OAuth provider. - // This block was added because of https://github.com/supertokens/supertokens-core/issues/295 - if (emailInfo.isVerified) { - const emailVerificationInstance = EmailVerification.getInstance(); - if (emailVerificationInstance) { - const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( - { - userId: response.user.id, - email: response.user.email, - userContext, - } - ); - - if (tokenResponse.status === "OK") { - await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ - token: tokenResponse.token, - userContext, - }); - } - } - } - - let session = await Session.createNewSession( - options.req, - options.res, - response.user.id, - {}, - {}, - userContext - ); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - session, - authCodeResponse: accessTokenAPIResponse.data, - }; - }, - - appleRedirectHandlerPOST: async function ({ code, state, options }): Promise { - const redirectURL = - options.appInfo.websiteDomain.getAsStringDangerous() + - options.appInfo.websiteBasePath.getAsStringDangerous() + - "/callback/apple?state=" + - state + - "&code=" + - code; - options.res.sendHTMLResponse( - `` - ); - }, - }; -} - -const DEV_OAUTH_AUTHORIZATION_URL = "https://supertokens.io/dev/oauth/redirect-to-provider"; -const DEV_OAUTH_REDIRECT_URL = "https://supertokens.io/dev/oauth/redirect-to-app"; - -// If Third Party login is used with one of the following development keys, then the dev authorization url and the redirect url will be used. -const DEV_OAUTH_CLIENT_IDS = [ - "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", // google - "467101b197249757c71f", // github -]; -const DEV_KEY_IDENTIFIER = "4398792-"; - -function isUsingDevelopmentClientId(client_id: string): boolean { - return client_id.startsWith(DEV_KEY_IDENTIFIER) || DEV_OAUTH_CLIENT_IDS.includes(client_id); -} - -export function getActualClientIdFromDevelopmentClientId(client_id: string): string { - if (client_id.startsWith(DEV_KEY_IDENTIFIER)) { - return client_id.split(DEV_KEY_IDENTIFIER)[1]; - } - return client_id; -} diff --git a/lib/ts/recipe/thirdparty/api/signinup.ts b/lib/ts/recipe/thirdparty/api/signinup.ts deleted file mode 100644 index fe6259619..000000000 --- a/lib/ts/recipe/thirdparty/api/signinup.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../error"; -import { send200Response } from "../../../utils"; -import { APIInterface, APIOptions } from "../"; -import { findRightProvider } from "../utils"; -import { makeDefaultUserContextFromAPI } from "../../../utils"; - -export default async function signInUpAPI(apiImplementation: APIInterface, options: APIOptions): Promise { - if (apiImplementation.signInUpPOST === undefined) { - return false; - } - - let bodyParams = await options.req.getJSONBody(); - let thirdPartyId = bodyParams.thirdPartyId; - let code = bodyParams.code === undefined ? "" : bodyParams.code; - let redirectURI = bodyParams.redirectURI; - let authCodeResponse = bodyParams.authCodeResponse; - let clientId = bodyParams.clientId; - - if (thirdPartyId === undefined || typeof thirdPartyId !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the thirdPartyId in request body", - }); - } - - if (typeof code !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please make sure that the code in the request body is a string", - }); - } - - if (code === "" && authCodeResponse === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide one of code or authCodeResponse in the request body", - }); - } - - if (authCodeResponse !== undefined && authCodeResponse.access_token === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the access_token inside the authCodeResponse request param", - }); - } - - if (redirectURI === undefined || typeof redirectURI !== "string") { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "Please provide the redirectURI in request body", - }); - } - - let provider = findRightProvider(options.providers, thirdPartyId, clientId); - if (provider === undefined) { - if (clientId === undefined) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: "The third party provider " + thirdPartyId + ` seems to be missing from the backend configs.`, - }); - } else { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: - "The third party provider " + - thirdPartyId + - ` seems to be missing from the backend configs. If it is configured, then please make sure that you are passing the correct clientId from the frontend.`, - }); - } - } - - let result = await apiImplementation.signInUpPOST({ - provider, - code, - clientId, - redirectURI, - options, - authCodeResponse, - userContext: makeDefaultUserContextFromAPI(options.req), - }); - - if (result.status === "OK") { - send200Response(options.res, { - status: result.status, - user: result.user, - createdNewUser: result.createdNewUser, - }); - } else if (result.status === "NO_EMAIL_GIVEN_BY_PROVIDER") { - send200Response(options.res, { - status: "NO_EMAIL_GIVEN_BY_PROVIDER", - }); - } else { - send200Response(options.res, result); - } - return true; -} diff --git a/lib/ts/recipe/thirdparty/constants.ts b/lib/ts/recipe/thirdparty/constants.ts deleted file mode 100644 index 7df6f7f2d..000000000 --- a/lib/ts/recipe/thirdparty/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -export const AUTHORISATION_API = "/authorisationurl"; - -export const SIGN_IN_UP_API = "/signinup"; - -export const APPLE_REDIRECT_HANDLER = "/callback/apple"; diff --git a/lib/ts/recipe/thirdparty/error.ts b/lib/ts/recipe/thirdparty/error.ts deleted file mode 100644 index dd6fbf793..000000000 --- a/lib/ts/recipe/thirdparty/error.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; - -export default class ThirdPartyError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }) { - super({ - ...options, - }); - this.fromRecipe = "thirdparty"; - } -} diff --git a/lib/ts/recipe/thirdparty/index.ts b/lib/ts/recipe/thirdparty/index.ts deleted file mode 100644 index c4af54b2d..000000000 --- a/lib/ts/recipe/thirdparty/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import * as thirdPartyProviders from "./providers"; -import { RecipeInterface, User, APIInterface, APIOptions, TypeProvider } from "./types"; - -export default class Wrapper { - static init = Recipe.init; - - static Error = SuperTokensError; - - static async signInUp(thirdPartyId: string, thirdPartyUserId: string, email: string, userContext: any = {}) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signInUp({ - thirdPartyId, - thirdPartyUserId, - email, - userContext, - }); - } - - static getUserById(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - - static getUsersByEmail(email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } - - static getUserByThirdPartyInfo(thirdPartyId: string, thirdPartyUserId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - userContext, - }); - } - - static Google = thirdPartyProviders.Google; - - static Github = thirdPartyProviders.Github; - - static Facebook = thirdPartyProviders.Facebook; - - static Apple = thirdPartyProviders.Apple; - - static Discord = thirdPartyProviders.Discord; - - static GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces; - - static Bitbucket = thirdPartyProviders.Bitbucket; - - static GitLab = thirdPartyProviders.GitLab; - - // static Okta = thirdPartyProviders.Okta; - - // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; -} - -export let init = Wrapper.init; - -export let Error = Wrapper.Error; - -export let signInUp = Wrapper.signInUp; - -export let getUserById = Wrapper.getUserById; - -export let getUsersByEmail = Wrapper.getUsersByEmail; - -export let getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; - -export let Google = Wrapper.Google; - -export let Github = Wrapper.Github; - -export let Facebook = Wrapper.Facebook; - -export let Apple = Wrapper.Apple; - -export let Discord = Wrapper.Discord; - -export let GoogleWorkspaces = Wrapper.GoogleWorkspaces; - -export let Bitbucket = Wrapper.Bitbucket; - -export let GitLab = Wrapper.GitLab; - -// export let Okta = Wrapper.Okta; - -// export let ActiveDirectory = Wrapper.ActiveDirectory; - -export type { RecipeInterface, User, APIInterface, APIOptions, TypeProvider }; diff --git a/lib/ts/recipe/thirdparty/providers/apple.ts b/lib/ts/recipe/thirdparty/providers/apple.ts deleted file mode 100644 index 0bc29c99d..000000000 --- a/lib/ts/recipe/thirdparty/providers/apple.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import { sign as jwtSign } from "jsonwebtoken"; -import STError from "../error"; -import { getActualClientIdFromDevelopmentClientId } from "../api/implementation"; -import SuperTokens from "../../../supertokens"; -import { APPLE_REDIRECT_HANDLER } from "../constants"; -import verifyAppleToken from "verify-apple-id-token"; - -type TypeThirdPartyProviderAppleConfig = { - clientId: string; - clientSecret: { - keyId: string; - privateKey: string; - teamId: string; - }; - scope?: string[]; - authorisationRedirect?: { - params?: { [key: string]: string | ((request: any) => string) }; - }; - isDefault?: boolean; -}; - -export default function Apple(config: TypeThirdPartyProviderAppleConfig): TypeProvider { - const id = "apple"; - - function getClientSecret(clientId: string, keyId: string, teamId: string, privateKey: string): string { - return jwtSign( - { - iss: teamId, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months - aud: "https://appleid.apple.com", - sub: getActualClientIdFromDevelopmentClientId(clientId), - }, - privateKey.replace(/\\n/g, "\n"), - { algorithm: "ES256", keyid: keyId } - ); - } - try { - // trying to generate a client secret, in case client has not passed the values correctly - getClientSecret( - config.clientId, - config.clientSecret.keyId, - config.clientSecret.teamId, - config.clientSecret.privateKey - ); - } catch (error) { - throw new STError({ - type: STError.BAD_INPUT_ERROR, - message: error.message, - }); - } - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let accessTokenAPIURL = "https://appleid.apple.com/auth/token"; - let clientSecret = getClientSecret( - config.clientId, - config.clientSecret.keyId, - config.clientSecret.teamId, - config.clientSecret.privateKey - ); - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://appleid.apple.com/auth/authorize"; - let scopes: string[] = ["email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - response_mode: "form_post", - response_type: "code", - client_id: config.clientId, - ...additionalParams, - }; - - async function getProfileInfo(accessTokenAPIResponse: { - access_token: string; - expires_in: number; - token_type: string; - refresh_token: string; - id_token: string; - }) { - /* - - Verify the JWS E256 signature using the server’s public key - - Verify the nonce for the authentication - - Verify that the iss field contains https://appleid.apple.com - - Verify that the aud field is the developer’s client_id - - Verify that the time is earlier than the exp value of the token */ - const payload = await verifyAppleToken({ - idToken: accessTokenAPIResponse.id_token, - clientId: getActualClientIdFromDevelopmentClientId(config.clientId), - }); - if (payload === null) { - throw new Error("no user info found from user's id token received from apple"); - } - let id = (payload as any).sub as string; - let email = (payload as any).email as string; - let isVerified = (payload as any).email_verified; - if (id === undefined || id === null) { - throw new Error("no user info found from user's id token received from apple"); - } - return { - id, - email: { - id: email, - isVerified, - }, - }; - } - function getRedirectURI() { - let supertokens = SuperTokens.getInstanceOrThrowError(); - return ( - supertokens.appInfo.apiDomain.getAsStringDangerous() + - supertokens.appInfo.apiBasePath.getAsStringDangerous() + - APPLE_REDIRECT_HANDLER - ); - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - getRedirectURI, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/bitbucket.ts b/lib/ts/recipe/thirdparty/providers/bitbucket.ts deleted file mode 100644 index bcb467286..000000000 --- a/lib/ts/recipe/thirdparty/providers/bitbucket.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import axios from "axios"; - -type TypeThirdPartyProviderBitbucketConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { [key: string]: string | ((request: any) => string) }; - }; - isDefault?: boolean; -}; - -export default function Bitbucket(config: TypeThirdPartyProviderBitbucketConfig): TypeProvider { - const id = "bitbucket"; - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let accessTokenAPIURL = "https://bitbucket.org/site/oauth2/access_token"; - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://bitbucket.org/site/oauth2/authorize"; - let scopes = ["account", "email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - access_type: "offline", - response_type: "code", - client_id: config.clientId, - ...additionalParams, - }; - - async function getProfileInfo(accessTokenAPIResponse: { - access_token: string; - expires_in: number; - token_type: string; - refresh_token?: string; - }) { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = await axios({ - method: "get", - url: "https://api.bitbucket.org/2.0/user", - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - let id = userInfo.uuid; - - let emailRes = await axios({ - method: "get", - url: "https://api.bitbucket.org/2.0/user/emails", - headers: { - Authorization: authHeader, - }, - }); - let emailData = emailRes.data; - let email = undefined; - let isVerified = false; - emailData.values.forEach((emailInfo: any) => { - if (emailInfo.is_primary) { - email = emailInfo.email; - isVerified = emailInfo.is_confirmed; - } - }); - - if (email === undefined) { - return { - id, - }; - } - return { - id, - email: { - id: email, - isVerified, - }, - }; - } - - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/discord.ts b/lib/ts/recipe/thirdparty/providers/discord.ts deleted file mode 100644 index d01451a35..000000000 --- a/lib/ts/recipe/thirdparty/providers/discord.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import axios from "axios"; - -type TypeThirdPartyProviderDiscordConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { [key: string]: string | ((request: any) => string) }; - }; - isDefault?: boolean; -}; - -export default function Discord(config: TypeThirdPartyProviderDiscordConfig): TypeProvider { - const id = "discord"; - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let accessTokenAPIURL = "https://discord.com/api/oauth2/token"; - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://discord.com/api/oauth2/authorize"; - let scopes = ["email", "identify"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - client_id: config.clientId, - response_type: "code", - ...additionalParams, - }; - - async function getProfileInfo(accessTokenAPIResponse: { - access_token: string; - expires_in: number; - token_type: string; - }) { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = await axios({ - method: "get", - url: "https://discord.com/api/users/@me", - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - return { - id: userInfo.id, - email: - userInfo.email === undefined - ? undefined - : { - id: userInfo.email, - isVerified: userInfo.verified, - }, - }; - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/facebook.ts b/lib/ts/recipe/thirdparty/providers/facebook.ts deleted file mode 100644 index c5a847766..000000000 --- a/lib/ts/recipe/thirdparty/providers/facebook.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import axios from "axios"; - -type TypeThirdPartyProviderFacebookConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - isDefault?: boolean; -}; - -export default function Facebook(config: TypeThirdPartyProviderFacebookConfig): TypeProvider { - const id = "facebook"; - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let accessTokenAPIURL = "https://graph.facebook.com/v9.0/oauth/access_token"; - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: config.clientSecret, - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://www.facebook.com/v9.0/dialog/oauth"; - let scopes = ["email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - response_type: "code", - client_id: config.clientId, - }; - - async function getProfileInfo(accessTokenAPIResponse: { - access_token: string; - expires_in: number; - token_type: string; - }) { - let accessToken = accessTokenAPIResponse.access_token; - let response = await axios({ - method: "get", - url: "https://graph.facebook.com/me", - params: { - access_token: accessToken, - fields: "id,email", - format: "json", - }, - }); - let userInfo = response.data; - let id = userInfo.id; - let email = userInfo.email; - if (email === undefined || email === null) { - return { - id, - }; - } - return { - id, - email: { - id: email, - isVerified: true, - }, - }; - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/github.ts b/lib/ts/recipe/thirdparty/providers/github.ts deleted file mode 100644 index 5e9128ae4..000000000 --- a/lib/ts/recipe/thirdparty/providers/github.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import axios from "axios"; - -type TypeThirdPartyProviderGithubConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { [key: string]: string | ((request: any) => string) }; - }; - isDefault?: boolean; -}; - -export default function Github(config: TypeThirdPartyProviderGithubConfig): TypeProvider { - const id = "github"; - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let accessTokenAPIURL = "https://github.com/login/oauth/access_token"; - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: config.clientSecret, - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://github.com/login/oauth/authorize"; - let scopes = ["read:user", "user:email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - client_id: config.clientId, - ...additionalParams, - }; - - async function getProfileInfo(accessTokenAPIResponse: { - access_token: string; - expires_in: number; - token_type: string; - }) { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = await axios({ - method: "get", - url: "https://api.github.com/user", - headers: { - Authorization: authHeader, - Accept: "application/vnd.github.v3+json", - }, - }); - let emailsInfoResponse = await axios({ - url: "https://api.github.com/user/emails", - headers: { - Authorization: authHeader, - Accept: "application/vnd.github.v3+json", - }, - }); - let userInfo = response.data; - let emailsInfo = emailsInfoResponse.data; - let id = userInfo.id.toString(); // github userId will be a number - /* - if user has choosen not to show their email publicly, userInfo here will - have email as null. So we instead get the info from the emails api and - use the email which is marked as primary one. - - Sample github response for email info - [ - { - email: '', - primary: true, - verified: true, - visibility: 'public' - } - ] - */ - let emailInfo = emailsInfo.find((e: any) => e.primary); - if (emailInfo === undefined) { - return { - id, - }; - } - let isVerified = emailInfo !== undefined ? emailInfo.verified : false; - return { - id, - email: - emailInfo.email === undefined - ? undefined - : { - id: emailInfo.email, - isVerified, - }, - }; - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/gitlab.ts b/lib/ts/recipe/thirdparty/providers/gitlab.ts deleted file mode 100644 index efe755631..000000000 --- a/lib/ts/recipe/thirdparty/providers/gitlab.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import axios from "axios"; -import NormalisedURLDomain from "../../../normalisedURLDomain"; - -type TypeThirdPartyProviderGitLabConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { [key: string]: string | ((request: any) => string) }; - }; - gitlabBaseUrl?: string; - isDefault?: boolean; -}; - -export default function GitLab(config: TypeThirdPartyProviderGitLabConfig): TypeProvider { - const id = "gitlab"; - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let baseUrl = - config.gitlabBaseUrl === undefined - ? "https://gitlab.com" // no traling slash cause we add that in the path - : new NormalisedURLDomain(config.gitlabBaseUrl).getAsStringDangerous(); - let accessTokenAPIURL = baseUrl + "/oauth/token"; - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = baseUrl + "/oauth/authorize"; - let scopes = ["read_user"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - response_type: "code", - client_id: config.clientId, - ...additionalParams, - }; - - async function getProfileInfo(accessTokenAPIResponse: { - access_token: string; - expires_in: number; - token_type: string; - refresh_token?: string; - }) { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = await axios({ - method: "get", - url: baseUrl + "/api/v4/user", - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - let id = userInfo.id + ""; - let email = userInfo.email; - if (email === undefined || email === null) { - return { - id, - }; - } - let isVerified = userInfo.confirmed_at !== null && userInfo.confirmed_at !== undefined; - return { - id, - email: { - id: email, - isVerified, - }, - }; - } - - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/google.ts b/lib/ts/recipe/thirdparty/providers/google.ts deleted file mode 100644 index e08376752..000000000 --- a/lib/ts/recipe/thirdparty/providers/google.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import axios from "axios"; - -type TypeThirdPartyProviderGoogleConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - authorisationRedirect?: { - params?: { [key: string]: string | ((request: any) => string) }; - }; - isDefault?: boolean; -}; - -export default function Google(config: TypeThirdPartyProviderGoogleConfig): TypeProvider { - const id = "google"; - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let accessTokenAPIURL = "https://oauth2.googleapis.com/token"; - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://accounts.google.com/o/oauth2/v2/auth"; - let scopes = ["https://www.googleapis.com/auth/userinfo.email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - client_id: config.clientId, - ...additionalParams, - }; - - async function getProfileInfo(accessTokenAPIResponse: { - access_token: string; - expires_in: number; - token_type: string; - scope: string; - refresh_token: string; - }) { - let accessToken = accessTokenAPIResponse.access_token; - let authHeader = `Bearer ${accessToken}`; - let response = await axios({ - method: "get", - url: "https://www.googleapis.com/oauth2/v1/userinfo", - params: { - alt: "json", - }, - headers: { - Authorization: authHeader, - }, - }); - let userInfo = response.data; - let id = userInfo.id; - let email = userInfo.email; - if (email === undefined || email === null) { - return { - id, - }; - } - let isVerified = userInfo.verified_email; - return { - id, - email: { - id: email, - isVerified, - }, - }; - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/googleWorkspaces.ts b/lib/ts/recipe/thirdparty/providers/googleWorkspaces.ts deleted file mode 100644 index 70d9d9216..000000000 --- a/lib/ts/recipe/thirdparty/providers/googleWorkspaces.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, TypeProviderGetResponse } from "../types"; -import { verifyIdTokenFromJWKSEndpoint } from "./utils"; -import { getActualClientIdFromDevelopmentClientId } from "../api/implementation"; - -type TypeThirdPartyProviderGoogleWorkspacesConfig = { - clientId: string; - clientSecret: string; - scope?: string[]; - domain?: string; - authorisationRedirect?: { - params?: { [key: string]: string | ((request: any) => string) }; - }; - isDefault?: boolean; -}; - -export default function GW(config: TypeThirdPartyProviderGoogleWorkspacesConfig): TypeProvider { - const id = "google-workspaces"; - let domain: string = config.domain === undefined ? "*" : config.domain; - - function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { - let accessTokenAPIURL = "https://oauth2.googleapis.com/token"; - let accessTokenAPIParams: { [key: string]: string } = { - client_id: config.clientId, - client_secret: config.clientSecret, - grant_type: "authorization_code", - }; - if (authCodeFromRequest !== undefined) { - accessTokenAPIParams.code = authCodeFromRequest; - } - if (redirectURI !== undefined) { - accessTokenAPIParams.redirect_uri = redirectURI; - } - let authorisationRedirectURL = "https://accounts.google.com/o/oauth2/v2/auth"; - let scopes = ["https://www.googleapis.com/auth/userinfo.email"]; - if (config.scope !== undefined) { - scopes = config.scope; - scopes = Array.from(new Set(scopes)); - } - let additionalParams = - config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined - ? {} - : config.authorisationRedirect.params; - let authorizationRedirectParams: { [key: string]: string } = { - scope: scopes.join(" "), - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - client_id: config.clientId, - hd: domain, - ...additionalParams, - }; - - async function getProfileInfo(authCodeResponse: { id_token: string }) { - let payload: any = await verifyIdTokenFromJWKSEndpoint( - authCodeResponse.id_token, - "https://www.googleapis.com/oauth2/v3/certs", - { - audience: getActualClientIdFromDevelopmentClientId(config.clientId), - issuer: ["https://accounts.google.com", "accounts.google.com"], - } - ); - - if (payload.email === undefined) { - throw new Error("Could not get email. Please use a different login method"); - } - - if (payload.hd === undefined) { - throw new Error("Please use a Google Workspace ID to login"); - } - - // if the domain is "*" in it, it means that any workspace email is allowed. - if (!domain.includes("*") && payload.hd !== domain) { - throw new Error("Please use emails from " + domain + " to login"); - } - - return { - id: payload.sub, - email: { - id: payload.email, - isVerified: payload.email_verified, - }, - }; - } - return { - accessTokenAPI: { - url: accessTokenAPIURL, - params: accessTokenAPIParams, - }, - authorisationRedirect: { - url: authorisationRedirectURL, - params: authorizationRedirectParams, - }, - getProfileInfo, - getClientId: () => { - return config.clientId; - }, - }; - } - - return { - id, - get, - isDefault: config.isDefault, - }; -} diff --git a/lib/ts/recipe/thirdparty/providers/index.ts b/lib/ts/recipe/thirdparty/providers/index.ts deleted file mode 100644 index af2dcee4c..000000000 --- a/lib/ts/recipe/thirdparty/providers/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import ProviderGoogle from "./google"; -import ProviderFacebook from "./facebook"; -import ProviderGithub from "./github"; -import ProviderApple from "./apple"; -import ProviderDiscord from "./discord"; -// import ProviderOkta from "./okta"; -import ProviderGoogleWorkspaces from "./googleWorkspaces"; -// import ProviderAD from "./activeDirectory"; -import ProviderBitbucket from "./bitbucket"; -import ProviderGitlab from "./gitlab"; - -export let Google = ProviderGoogle; -export let Facebook = ProviderFacebook; -export let Github = ProviderGithub; -export let Apple = ProviderApple; -export let Discord = ProviderDiscord; -export let GoogleWorkspaces = ProviderGoogleWorkspaces; -export let Bitbucket = ProviderBitbucket; -export let GitLab = ProviderGitlab; -// export let Okta = ProviderOkta; -// export let ActiveDirectory = ProviderAD; diff --git a/lib/ts/recipe/thirdparty/providers/utils.ts b/lib/ts/recipe/thirdparty/providers/utils.ts deleted file mode 100644 index b81588cee..000000000 --- a/lib/ts/recipe/thirdparty/providers/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { verify, VerifyOptions } from "jsonwebtoken"; -import jwksClient from "jwks-rsa"; - -export async function verifyIdTokenFromJWKSEndpoint( - idToken: string, - jwksUri: string, - otherOptions: VerifyOptions -): Promise { - const client = jwksClient({ - jwksUri, - }); - function getKey(header: any, callback: any) { - client.getSigningKey(header.kid, function (_, key: any) { - var signingKey = key.publicKey || key.rsaPublicKey; - callback(null, signingKey); - }); - } - - let payload: any = await new Promise((resolve, reject) => { - verify(idToken, getKey, otherOptions, function (err, decoded) { - if (err) { - reject(err); - } else { - resolve(decoded); - } - }); - }); - - return payload; -} diff --git a/lib/ts/recipe/thirdparty/recipe.ts b/lib/ts/recipe/thirdparty/recipe.ts deleted file mode 100644 index 892561f67..000000000 --- a/lib/ts/recipe/thirdparty/recipe.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import { TypeInput, TypeNormalisedInput, TypeProvider, RecipeInterface, APIInterface } from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import EmailVerificationRecipe from "../emailverification/recipe"; -import STError from "./error"; - -import { SIGN_IN_UP_API, AUTHORISATION_API, APPLE_REDIRECT_HANDLER } from "./constants"; -import NormalisedURLPath from "../../normalisedURLPath"; -import signInUpAPI from "./api/signinup"; -import authorisationUrlAPI from "./api/authorisationUrl"; -import RecipeImplementation from "./recipeImplementation"; -import APIImplementation from "./api/implementation"; -import { Querier } from "../../querier"; -import { BaseRequest, BaseResponse } from "../../framework"; -import appleRedirectHandler from "./api/appleRedirect"; -import OverrideableBuilder from "supertokens-js-override"; -import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; -import { GetEmailForUserIdFunc } from "../emailverification/types"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "thirdparty"; - - config: TypeNormalisedInput; - - providers: TypeProvider[]; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - _recipes: {}, - _ingredients: {} - ) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - - this.providers = this.config.signInAndUpFeature.providers; - - { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - - PostSuperTokensInitCallbacks.addPostInitCallback(() => { - const emailVerificationRecipe = EmailVerificationRecipe.getInstance(); - if (emailVerificationRecipe !== undefined) { - emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)); - } - }); - } - - static init(config: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe( - Recipe.RECIPE_ID, - appInfo, - isInServerlessEnv, - config, - {}, - { - emailDelivery: undefined, - } - ); - return Recipe.instance; - } else { - throw new Error("ThirdParty recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - getAPIsHandled = (): APIHandled[] => { - return [ - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(SIGN_IN_UP_API), - id: SIGN_IN_UP_API, - disabled: this.apiImpl.signInUpPOST === undefined, - }, - { - method: "get", - pathWithoutApiBasePath: new NormalisedURLPath(AUTHORISATION_API), - id: AUTHORISATION_API, - disabled: this.apiImpl.authorisationUrlGET === undefined, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath(APPLE_REDIRECT_HANDLER), - id: APPLE_REDIRECT_HANDLER, - disabled: this.apiImpl.appleRedirectHandlerPOST === undefined, - }, - ]; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - _path: NormalisedURLPath, - _method: HTTPMethod - ): Promise => { - let options = { - config: this.config, - recipeId: this.getRecipeId(), - isInServerlessEnv: this.isInServerlessEnv, - recipeImplementation: this.recipeInterfaceImpl, - providers: this.providers, - req, - res, - appInfo: this.getAppInfo(), - }; - if (id === SIGN_IN_UP_API) { - return await signInUpAPI(this.apiImpl, options); - } else if (id === AUTHORISATION_API) { - return await authorisationUrlAPI(this.apiImpl, options); - } else if (id === APPLE_REDIRECT_HANDLER) { - return await appleRedirectHandler(this.apiImpl, options); - } - return false; - }; - - handleError = async (err: STError, _request: BaseRequest, _response: BaseResponse): Promise => { - throw err; - }; - - getAllCORSHeaders = (): string[] => { - return []; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - }; - - // helper functions... - getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - let userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; - } - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - }; -} diff --git a/lib/ts/recipe/thirdparty/recipeImplementation.ts b/lib/ts/recipe/thirdparty/recipeImplementation.ts deleted file mode 100644 index 3c539458d..000000000 --- a/lib/ts/recipe/thirdparty/recipeImplementation.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { RecipeInterface, User } from "./types"; -import { Querier } from "../../querier"; -import NormalisedURLPath from "../../normalisedURLPath"; - -export default function getRecipeImplementation(querier: Querier): RecipeInterface { - return { - signInUp: async function ({ - thirdPartyId, - thirdPartyUserId, - email, - }: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/signinup"), { - thirdPartyId, - thirdPartyUserId, - email: { id: email }, - }); - return { - status: "OK", - createdNewUser: response.createdNewUser, - user: response.user, - }; - }, - - getUserById: async function ({ userId }: { userId: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - userId, - }); - if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; - } - }, - - getUsersByEmail: async function ({ email }: { email: string }): Promise { - const { users } = await querier.sendGetRequest(new NormalisedURLPath("/recipe/users/by-email"), { - email, - }); - - return users; - }, - - getUserByThirdPartyInfo: async function ({ - thirdPartyId, - thirdPartyUserId, - }: { - thirdPartyId: string; - thirdPartyUserId: string; - }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - thirdPartyId, - thirdPartyUserId, - }); - if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; - } - }, - }; -} diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts deleted file mode 100644 index 5a26d7fe3..000000000 --- a/lib/ts/recipe/thirdparty/types.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { GeneralErrorResponse } from "../../types"; - -export type UserInfo = { id: string; email?: { id: string; isVerified: boolean } }; - -export type TypeProviderGetResponse = { - accessTokenAPI: { - url: string; - params: { [key: string]: string }; // Will be merged with our object - }; - authorisationRedirect: { - url: string; - params: { [key: string]: string | ((request: any) => string) }; - }; - getProfileInfo: (authCodeResponse: any, userContext: any) => Promise; - getClientId: (userContext: any) => string; - getRedirectURI?: (userContext: any) => string; // if undefined, the redirect_uri is set on the frontend. -}; - -export type TypeProvider = { - id: string; - get: ( - redirectURI: string | undefined, - authCodeFromRequest: string | undefined, - userContext: any - ) => TypeProviderGetResponse; - isDefault?: boolean; // if not present, we treat it as false -}; - -export type User = { - // https://github.com/supertokens/core-driver-interface/wiki#third-party-user - id: string; - timeJoined: number; - email: string; - thirdParty: { - id: string; - userId: string; - }; -}; - -export type TypeInputSignInAndUp = { - providers: TypeProvider[]; -}; - -export type TypeNormalisedInputSignInAndUp = { - providers: TypeProvider[]; -}; - -export type TypeInput = { - signInAndUpFeature: TypeInputSignInAndUp; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - signInAndUpFeature: TypeNormalisedInputSignInAndUp; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - - getUsersByEmail(input: { email: string; userContext: any }): Promise; - - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise; - - signInUp(input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }>; -}; - -export type APIOptions = { - recipeImplementation: RecipeInterface; - config: TypeNormalisedInput; - recipeId: string; - isInServerlessEnv: boolean; - providers: TypeProvider[]; - req: BaseRequest; - res: BaseResponse; - appInfo: NormalisedAppinfo; -}; - -export type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - provider: TypeProvider; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - url: string; - } - | GeneralErrorResponse - >); - - signInUpPOST: - | undefined - | ((input: { - provider: TypeProvider; - code: string; - redirectURI: string; - authCodeResponse?: any; - clientId?: string; - options: APIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - authCodeResponse: any; - } - | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } - | GeneralErrorResponse - >); - - appleRedirectHandlerPOST: - | undefined - | ((input: { code: string; state: string; options: APIOptions; userContext: any }) => Promise); -}; diff --git a/lib/ts/recipe/thirdparty/utils.ts b/lib/ts/recipe/thirdparty/utils.ts deleted file mode 100644 index 7a6e60238..000000000 --- a/lib/ts/recipe/thirdparty/utils.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { NormalisedAppinfo } from "../../types"; -import { RecipeInterface, APIInterface, TypeProvider } from "./types"; -import { TypeInput, TypeNormalisedInput, TypeInputSignInAndUp, TypeNormalisedInputSignInAndUp } from "./types"; - -export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { - let signInAndUpFeature = validateAndNormaliseSignInAndUpConfig(appInfo, config.signInAndUpFeature); - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config.override, - }; - - return { - signInAndUpFeature, - override, - }; -} - -export function findRightProvider( - providers: TypeProvider[], - thirdPartyId: string, - clientId?: string -): TypeProvider | undefined { - return providers.find((p) => { - let id = p.id; - if (id !== thirdPartyId) { - return false; - } - - // first if there is only one provider with thirdPartyId in the providers array, - let otherProvidersWithSameId = providers.filter((p1) => p1.id === id && p !== p1); - if (otherProvidersWithSameId.length === 0) { - // they we always return that. - return true; - } - - // otherwise, we look for the isDefault provider if clientId is missing - if (clientId === undefined) { - return p.isDefault === true; - } - - // otherwise, we return a provider that matches based on client ID as well. - return p.get(undefined, undefined, {}).getClientId({}) === clientId; - }); -} - -function validateAndNormaliseSignInAndUpConfig( - _: NormalisedAppinfo, - config: TypeInputSignInAndUp -): TypeNormalisedInputSignInAndUp { - let providers = config.providers; - - if (providers === undefined || providers.length === 0) { - throw new Error( - "thirdparty recipe requires atleast 1 provider to be passed in signInAndUpFeature.providers config" - ); - } - - // we check if there are multiple providers with the same id that have isDefault as true. - // In this case, we want to throw an error.. - let isDefaultProvidersSet = new Set(); - let allProvidersSet = new Set(); - providers.forEach((p) => { - let id = p.id; - allProvidersSet.add(p.id); - let isDefault = p.isDefault; - - if (isDefault === undefined) { - // if this id is not being used by any other provider, we treat this as the isDefault - let otherProvidersWithSameId = providers.filter((p1) => p1.id === id && p !== p1); - if (otherProvidersWithSameId.length === 0) { - // we treat this as the isDefault now... - isDefault = true; - } - } - if (isDefault) { - if (isDefaultProvidersSet.has(id)) { - throw new Error( - `You have provided multiple third party providers that have the id: "${id}" and are marked as "isDefault: true". Please only mark one of them as isDefault.` - ); - } - isDefaultProvidersSet.add(id); - } - }); - - if (isDefaultProvidersSet.size !== allProvidersSet.size) { - // this means that there is no provider marked as isDefault - throw new Error( - `The providers array has multiple entries for the same third party provider. Please mark one of them as the default one by using "isDefault: true".` - ); - } - - return { - providers, - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.ts deleted file mode 100644 index f6fcc2f5d..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { APIInterface } from "../../emailpassword"; -import { APIInterface as ThirdPartyEmailPasswordAPIInterface } from "../"; - -export default function getIterfaceImpl(apiImplmentation: ThirdPartyEmailPasswordAPIInterface): APIInterface { - return { - emailExistsGET: apiImplmentation.emailPasswordEmailExistsGET?.bind(apiImplmentation), - generatePasswordResetTokenPOST: apiImplmentation.generatePasswordResetTokenPOST?.bind(apiImplmentation), - passwordResetPOST: apiImplmentation.passwordResetPOST?.bind(apiImplmentation), - signInPOST: apiImplmentation.emailPasswordSignInPOST?.bind(apiImplmentation), - signUpPOST: apiImplmentation.emailPasswordSignUpPOST?.bind(apiImplmentation), - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/api/implementation.ts b/lib/ts/recipe/thirdpartyemailpassword/api/implementation.ts deleted file mode 100644 index 534db5c56..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/api/implementation.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { APIInterface } from "../"; -import EmailPasswordAPIImplementation from "../../emailpassword/api/implementation"; -import ThirdPartyAPIImplementation from "../../thirdparty/api/implementation"; -import DerivedEP from "./emailPasswordAPIImplementation"; -import DerivedTP from "./thirdPartyAPIImplementation"; - -export default function getAPIImplementation(): APIInterface { - let emailPasswordImplementation = EmailPasswordAPIImplementation(); - let thirdPartyImplementation = ThirdPartyAPIImplementation(); - return { - emailPasswordEmailExistsGET: emailPasswordImplementation.emailExistsGET?.bind(DerivedEP(this)), - authorisationUrlGET: thirdPartyImplementation.authorisationUrlGET?.bind(DerivedTP(this)), - emailPasswordSignInPOST: emailPasswordImplementation.signInPOST?.bind(DerivedEP(this)), - emailPasswordSignUpPOST: emailPasswordImplementation.signUpPOST?.bind(DerivedEP(this)), - generatePasswordResetTokenPOST: emailPasswordImplementation.generatePasswordResetTokenPOST?.bind( - DerivedEP(this) - ), - passwordResetPOST: emailPasswordImplementation.passwordResetPOST?.bind(DerivedEP(this)), - thirdPartySignInUpPOST: thirdPartyImplementation.signInUpPOST?.bind(DerivedTP(this)), - appleRedirectHandlerPOST: thirdPartyImplementation.appleRedirectHandlerPOST?.bind(DerivedTP(this)), - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts deleted file mode 100644 index eb5066d51..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { APIInterface } from "../../thirdparty"; -import { APIInterface as ThirdPartyEmailPasswordAPIInterface } from "../"; - -export default function getIterfaceImpl(apiImplmentation: ThirdPartyEmailPasswordAPIInterface): APIInterface { - const signInUpPOSTFromThirdPartyEmailPassword = apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation); - return { - authorisationUrlGET: apiImplmentation.authorisationUrlGET?.bind(apiImplmentation), - appleRedirectHandlerPOST: apiImplmentation.appleRedirectHandlerPOST?.bind(apiImplmentation), - signInUpPOST: - signInUpPOSTFromThirdPartyEmailPassword === undefined - ? undefined - : async function (input) { - let result = await signInUpPOSTFromThirdPartyEmailPassword(input); - if (result.status === "OK") { - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return { - ...result, - user: { - ...result.user, - thirdParty: { - ...result.user.thirdParty, - }, - }, - }; - } - return result; - }, - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts deleted file mode 100644 index e6c6f07b1..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeThirdPartyEmailPasswordEmailDeliveryInput, User } from "../../../types"; -import { RecipeInterface as EmailPasswordRecipeInterface } from "../../../../emailpassword"; -import { NormalisedAppinfo } from "../../../../../types"; -import EmailPasswordBackwardCompatibilityService from "../../../../emailpassword/emaildelivery/services/backwardCompatibility"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; - -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private emailPasswordBackwardCompatibilityService: EmailPasswordBackwardCompatibilityService; - - constructor( - emailPasswordRecipeInterfaceImpl: EmailPasswordRecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } - ) { - { - this.emailPasswordBackwardCompatibilityService = new EmailPasswordBackwardCompatibilityService( - emailPasswordRecipeInterfaceImpl, - appInfo, - isInServerlessEnv, - resetPasswordUsingTokenFeature - ); - } - } - - sendEmail = async (input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: any }) => { - await this.emailPasswordBackwardCompatibilityService.sendEmail(input); - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/index.ts b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/index.ts deleted file mode 100644 index 5d393e47d..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import SMTP from "./smtp"; -export let SMTPService = SMTP; diff --git a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts deleted file mode 100644 index 2534012c1..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; -import EmailPasswordSMTPService from "../../../../emailpassword/emaildelivery/services/smtp"; - -export default class SMTPService implements EmailDeliveryInterface { - private emailPasswordSMTPService: EmailPasswordSMTPService; - - constructor(config: TypeInput) { - this.emailPasswordSMTPService = new EmailPasswordSMTPService(config); - } - - sendEmail = async (input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: any }) => { - await this.emailPasswordSMTPService.sendEmail(input); - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/error.ts b/lib/ts/recipe/thirdpartyemailpassword/error.ts deleted file mode 100644 index 8ebcb26bf..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/error.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; - -export default class ThirdPartyEmailPasswordError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }) { - super({ - ...options, - }); - this.fromRecipe = "thirdpartyemailpassword"; - } -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/index.ts b/lib/ts/recipe/thirdpartyemailpassword/index.ts deleted file mode 100644 index 3828b0e5f..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/index.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import * as thirdPartyProviders from "../thirdparty/providers"; -import { RecipeInterface, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions } from "./types"; -import { TypeProvider } from "../thirdparty/types"; -import { TypeEmailPasswordEmailDeliveryInput } from "../emailpassword/types"; - -export default class Wrapper { - static init = Recipe.init; - - static Error = SuperTokensError; - - static thirdPartySignInUp(thirdPartyId: string, thirdPartyUserId: string, email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartySignInUp({ - thirdPartyId, - thirdPartyUserId, - email, - userContext, - }); - } - - static getUserByThirdPartyInfo(thirdPartyId: string, thirdPartyUserId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - userContext, - }); - } - - static emailPasswordSignUp(email: string, password: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignUp({ - email, - password, - userContext, - }); - } - - static emailPasswordSignIn(email: string, password: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignIn({ - email, - password, - userContext, - }); - } - - static getUserById(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - - static getUsersByEmail(email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } - - static createResetPasswordToken(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, userContext }); - } - - static resetPasswordUsingToken(token: string, newPassword: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ - token, - newPassword, - userContext, - }); - } - - static updateEmailOrPassword(input: { userId: string; email?: string; password?: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ - userContext: {}, - ...input, - }); - } - - static Google = thirdPartyProviders.Google; - - static Github = thirdPartyProviders.Github; - - static Facebook = thirdPartyProviders.Facebook; - - static Apple = thirdPartyProviders.Apple; - - static Discord = thirdPartyProviders.Discord; - - static GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces; - - static Bitbucket = thirdPartyProviders.Bitbucket; - - static GitLab = thirdPartyProviders.GitLab; - - // static Okta = thirdPartyProviders.Okta; - - // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; - - static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any }) { - return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, - ...input, - }); - } -} - -export let init = Wrapper.init; - -export let Error = Wrapper.Error; - -export let emailPasswordSignUp = Wrapper.emailPasswordSignUp; - -export let emailPasswordSignIn = Wrapper.emailPasswordSignIn; - -export let thirdPartySignInUp = Wrapper.thirdPartySignInUp; - -export let getUserById = Wrapper.getUserById; - -export let getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; - -export let getUsersByEmail = Wrapper.getUsersByEmail; - -export let createResetPasswordToken = Wrapper.createResetPasswordToken; - -export let resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; - -export let updateEmailOrPassword = Wrapper.updateEmailOrPassword; - -export let Google = Wrapper.Google; - -export let Github = Wrapper.Github; - -export let Facebook = Wrapper.Facebook; - -export let Apple = Wrapper.Apple; - -export let Discord = Wrapper.Discord; - -export let GoogleWorkspaces = Wrapper.GoogleWorkspaces; - -export let Bitbucket = Wrapper.Bitbucket; - -export let GitLab = Wrapper.GitLab; - -// export let Okta = Wrapper.Okta; - -// export let ActiveDirectory = Wrapper.ActiveDirectory; - -export type { RecipeInterface, TypeProvider, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions }; - -export let sendEmail = Wrapper.sendEmail; diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts b/lib/ts/recipe/thirdpartyemailpassword/recipe.ts deleted file mode 100644 index d796181de..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import EmailPasswordRecipe from "../emailpassword/recipe"; -import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; -import STError from "./error"; -import { - TypeInput, - TypeNormalisedInput, - RecipeInterface, - APIInterface, - TypeThirdPartyEmailPasswordEmailDeliveryInput, -} from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import STErrorEmailPassword from "../emailpassword/error"; -import STErrorThirdParty from "../thirdparty/error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import RecipeImplementation from "./recipeImplementation"; -import EmailPasswordRecipeImplementation from "./recipeImplementation/emailPasswordRecipeImplementation"; -import ThirdPartyRecipeImplementation from "./recipeImplementation/thirdPartyRecipeImplementation"; -import getThirdPartyIterfaceImpl from "./api/thirdPartyAPIImplementation"; -import getEmailPasswordIterfaceImpl from "./api/emailPasswordAPIImplementation"; -import APIImplementation from "./api/implementation"; -import { Querier } from "../../querier"; -import OverrideableBuilder from "supertokens-js-override"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "thirdpartyemailpassword"; - - config: TypeNormalisedInput; - - private emailPasswordRecipe: EmailPasswordRecipe; - - private thirdPartyRecipe: ThirdPartyRecipe | undefined; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - isInServerlessEnv: boolean; - - emailDelivery: EmailDeliveryIngredient; - - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - recipes: { - thirdPartyInstance: ThirdPartyRecipe | undefined; - emailPasswordInstance: EmailPasswordRecipe | undefined; - }, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - } - ) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - { - let builder = new OverrideableBuilder( - RecipeImplementation( - Querier.getNewInstanceOrThrowError(EmailPasswordRecipe.RECIPE_ID), - Querier.getNewInstanceOrThrowError(ThirdPartyRecipe.RECIPE_ID) - ) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - - let emailPasswordRecipeImplementation = EmailPasswordRecipeImplementation(this.recipeInterfaceImpl); - /** - * emailDelivery will always needs to be declared after isInServerlessEnv - * and recipeInterfaceImpl values are set - */ - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new EmailDeliveryIngredient( - this.config.getEmailDeliveryConfig(emailPasswordRecipeImplementation, this.isInServerlessEnv) - ) - : ingredients.emailDelivery; - - this.emailPasswordRecipe = - recipes.emailPasswordInstance !== undefined - ? recipes.emailPasswordInstance - : new EmailPasswordRecipe( - recipeId, - appInfo, - isInServerlessEnv, - { - override: { - functions: (_) => { - return emailPasswordRecipeImplementation; - }, - apis: (_) => { - return getEmailPasswordIterfaceImpl(this.apiImpl); - }, - }, - signUpFeature: { - formFields: this.config.signUpFeature.formFields, - }, - resetPasswordUsingTokenFeature: this.config.resetPasswordUsingTokenFeature, - }, - { - emailDelivery: this.emailDelivery, - } - ); - - if (this.config.providers.length !== 0) { - this.thirdPartyRecipe = - recipes.thirdPartyInstance !== undefined - ? recipes.thirdPartyInstance - : new ThirdPartyRecipe( - recipeId, - appInfo, - isInServerlessEnv, - { - override: { - functions: (_) => { - return ThirdPartyRecipeImplementation(this.recipeInterfaceImpl); - }, - apis: (_) => { - return getThirdPartyIterfaceImpl(this.apiImpl); - }, - }, - signInAndUpFeature: { - providers: this.config.providers, - }, - }, - {}, - { - emailDelivery: this.emailDelivery, - } - ); - } - } - - static init(config: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe( - Recipe.RECIPE_ID, - appInfo, - isInServerlessEnv, - config, - { - emailPasswordInstance: undefined, - thirdPartyInstance: undefined, - }, - { - emailDelivery: undefined, - } - ); - return Recipe.instance; - } else { - throw new Error( - "ThirdPartyEmailPassword recipe has already been initialised. Please check your code for bugs." - ); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - getAPIsHandled = (): APIHandled[] => { - let apisHandled = [...this.emailPasswordRecipe.getAPIsHandled()]; - if (this.thirdPartyRecipe !== undefined) { - apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()); - } - return apisHandled; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ): Promise => { - if (this.emailPasswordRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined) { - return await this.emailPasswordRecipe.handleAPIRequest(id, req, res, path, method); - } - if ( - this.thirdPartyRecipe !== undefined && - this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined - ) { - return await this.thirdPartyRecipe.handleAPIRequest(id, req, res, path, method); - } - return false; - }; - - handleError = async ( - err: STErrorEmailPassword | STErrorThirdParty, - request: BaseRequest, - response: BaseResponse - ): Promise => { - if (err.fromRecipe === Recipe.RECIPE_ID) { - throw err; - } else { - if (this.emailPasswordRecipe.isErrorFromThisRecipe(err)) { - return await this.emailPasswordRecipe.handleError(err, request, response); - } else if (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err)) { - return await this.thirdPartyRecipe.handleError(err, request, response); - } - throw err; - } - }; - - getAllCORSHeaders = (): string[] => { - let corsHeaders = [...this.emailPasswordRecipe.getAllCORSHeaders()]; - if (this.thirdPartyRecipe !== undefined) { - corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()); - } - return corsHeaders; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return ( - STError.isErrorFromSuperTokens(err) && - (err.fromRecipe === Recipe.RECIPE_ID || - this.emailPasswordRecipe.isErrorFromThisRecipe(err) || - (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err))) - ); - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts deleted file mode 100644 index c57671338..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { RecipeInterface, User } from "../../emailpassword/types"; -import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; - -export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { - return { - signUp: async function (input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { - return await recipeInterface.emailPasswordSignUp(input); - }, - - signIn: async function (input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { - return recipeInterface.emailPasswordSignIn(input); - }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty !== undefined) { - // either user is undefined or it's a thirdparty user. - return undefined; - } - return user; - }, - - getUserByEmail: async function (input: { email: string; userContext: any }): Promise { - let result = await recipeInterface.getUsersByEmail(input); - for (let i = 0; i < result.length; i++) { - if (result[i].thirdParty === undefined) { - return result[i]; - } - } - return undefined; - }, - - createResetPasswordToken: async function (input: { - userId: string; - userContext: any; - }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - return recipeInterface.createResetPasswordToken(input); - }, - - resetPasswordUsingToken: async function (input: { token: string; newPassword: string; userContext: any }) { - return recipeInterface.resetPasswordUsingToken(input); - }, - - updateEmailOrPassword: async function (input: { - userId: string; - email?: string; - password?: string; - userContext: any; - }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" }> { - return recipeInterface.updateEmailOrPassword(input); - }, - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts deleted file mode 100644 index 41b2ee9a7..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { RecipeInterface, User } from "../types"; -import EmailPasswordImplemenation from "../../emailpassword/recipeImplementation"; - -import ThirdPartyImplemenation from "../../thirdparty/recipeImplementation"; -import { RecipeInterface as ThirdPartyRecipeInterface } from "../../thirdparty"; -import { Querier } from "../../../querier"; -import DerivedEP from "./emailPasswordRecipeImplementation"; -import DerivedTP from "./thirdPartyRecipeImplementation"; - -export default function getRecipeInterface( - emailPasswordQuerier: Querier, - thirdPartyQuerier?: Querier -): RecipeInterface { - let originalEmailPasswordImplementation = EmailPasswordImplemenation(emailPasswordQuerier); - let originalThirdPartyImplementation: undefined | ThirdPartyRecipeInterface; - if (thirdPartyQuerier !== undefined) { - originalThirdPartyImplementation = ThirdPartyImplemenation(thirdPartyQuerier); - } - - return { - emailPasswordSignUp: async function (input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { - return await originalEmailPasswordImplementation.signUp.bind(DerivedEP(this))(input); - }, - - emailPasswordSignIn: async function (input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { - return originalEmailPasswordImplementation.signIn.bind(DerivedEP(this))(input); - }, - - thirdPartySignInUp: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { - if (originalThirdPartyImplementation === undefined) { - throw new Error("No thirdparty provider configured"); - } - return originalThirdPartyImplementation.signInUp.bind(DerivedTP(this))(input); - }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user: User | undefined = await originalEmailPasswordImplementation.getUserById.bind(DerivedEP(this))( - input - ); - if (user !== undefined) { - return user; - } - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return await originalThirdPartyImplementation.getUserById.bind(DerivedTP(this))(input); - }, - - getUsersByEmail: async function ({ email, userContext }: { email: string; userContext: any }): Promise { - let userFromEmailPass: User | undefined = await originalEmailPasswordImplementation.getUserByEmail.bind( - DerivedEP(this) - )({ email, userContext }); - - if (originalThirdPartyImplementation === undefined) { - return userFromEmailPass === undefined ? [] : [userFromEmailPass]; - } - let usersFromThirdParty: User[] = await originalThirdPartyImplementation.getUsersByEmail.bind( - DerivedTP(this) - )({ email, userContext }); - - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }, - - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise { - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind(DerivedTP(this))(input); - }, - - createResetPasswordToken: async function (input: { - userId: string; - userContext: any; - }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - return originalEmailPasswordImplementation.createResetPasswordToken.bind(DerivedEP(this))(input); - }, - - resetPasswordUsingToken: async function (input: { token: string; newPassword: string; userContext: any }) { - return originalEmailPasswordImplementation.resetPasswordUsingToken.bind(DerivedEP(this))(input); - }, - - updateEmailOrPassword: async function ( - this: RecipeInterface, - input: { - userId: string; - email?: string; - password?: string; - userContext: any; - } - ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" }> { - let user = await this.getUserById({ userId: input.userId, userContext: input.userContext }); - if (user === undefined) { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } else if (user.thirdParty !== undefined) { - throw new Error("Cannot update email or password of a user who signed up using third party login."); - } - return originalEmailPasswordImplementation.updateEmailOrPassword.bind(DerivedEP(this))(input); - }, - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts deleted file mode 100644 index ef50b61e3..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { RecipeInterface, User } from "../../thirdparty/types"; -import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; - -export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { - return { - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise { - let user = await recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || user.thirdParty === undefined) { - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - }; - }, - - signInUp: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { - let result = await recipeInterface.thirdPartySignInUp(input); - if (result.user.thirdParty === undefined) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: { - email: result.user.email, - id: result.user.id, - timeJoined: result.user.timeJoined, - thirdParty: result.user.thirdParty, - }, - }; - }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty === undefined) { - // either user is undefined or it's an email password user. - return undefined; - } - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - }; - }, - - getUsersByEmail: async function (input: { email: string; userContext: any }): Promise { - let users = await recipeInterface.getUsersByEmail(input); - - // we filter out all non thirdparty users. - return users.filter((u) => { - return u.thirdParty !== undefined; - }) as User[]; - }, - }; -} diff --git a/lib/ts/recipe/thirdpartyemailpassword/types.ts b/lib/ts/recipe/thirdpartyemailpassword/types.ts deleted file mode 100644 index 7a00acc34..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/types.ts +++ /dev/null @@ -1,295 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, APIOptions as ThirdPartyAPIOptionsOriginal } from "../thirdparty/types"; -import { - NormalisedFormField, - TypeFormField, - TypeInputFormField, - TypeInputResetPasswordUsingTokenFeature, - APIOptions as EmailPasswordAPIOptionsOriginal, - TypeEmailPasswordEmailDeliveryInput, - RecipeInterface as EPRecipeInterface, -} from "../emailpassword/types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse } from "../../types"; - -export type User = { - id: string; - timeJoined: number; - email: string; - thirdParty?: { - id: string; - userId: string; - }; -}; - -export type TypeContextEmailPasswordSignUp = { - loginType: "emailpassword"; - formFields: TypeFormField[]; -}; - -export type TypeContextEmailPasswordSignIn = { - loginType: "emailpassword"; -}; - -export type TypeContextThirdParty = { - loginType: "thirdparty"; - thirdPartyAuthCodeResponse: any; -}; - -export type TypeInputSignUp = { - formFields?: TypeInputFormField[]; -}; - -export type TypeNormalisedInputSignUp = { - formFields: NormalisedFormField[]; -}; - -export type TypeInput = { - signUpFeature?: TypeInputSignUp; - providers?: TypeProvider[]; - emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - signUpFeature: TypeNormalisedInputSignUp; - providers: TypeProvider[]; - getEmailDeliveryConfig: ( - emailPasswordRecipeImpl: EPRecipeInterface, - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - - getUsersByEmail(input: { email: string; userContext: any }): Promise; - - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise; - - thirdPartySignInUp(input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }>; - - emailPasswordSignUp(input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>; - - emailPasswordSignIn(input: { - email: string; - password: string; - userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>; - - createResetPasswordToken(input: { - userId: string; - userContext: any; - }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; - - resetPasswordUsingToken(input: { - token: string; - newPassword: string; - userContext: any; - }): Promise< - | { - status: "OK"; - /** - * The id of the user whose password was reset. - * Defined for Core versions 3.9 or later - */ - userId?: string; - } - | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } - >; - - updateEmailOrPassword(input: { - userId: string; - email?: string; - password?: string; - userContext: any; - }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" }>; -}; - -export type EmailPasswordAPIOptions = EmailPasswordAPIOptionsOriginal; - -export type ThirdPartyAPIOptions = ThirdPartyAPIOptionsOriginal; -export type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - provider: TypeProvider; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - url: string; - } - | GeneralErrorResponse - >); - - emailPasswordEmailExistsGET: - | undefined - | ((input: { - email: string; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); - - generatePasswordResetTokenPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - } - | GeneralErrorResponse - >); - - passwordResetPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - token: string; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - userId?: string; - } - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; - } - | GeneralErrorResponse - >); - - thirdPartySignInUpPOST: - | undefined - | ((input: { - provider: TypeProvider; - code: string; - redirectURI: string; - authCodeResponse?: any; - clientId?: string; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - authCodeResponse: any; - } - | GeneralErrorResponse - | { - status: "NO_EMAIL_GIVEN_BY_PROVIDER"; - } - >); - - emailPasswordSignInPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - | GeneralErrorResponse - >); - - emailPasswordSignUpPOST: - | undefined - | ((input: { - formFields: { - id: string; - value: string; - }[]; - options: EmailPasswordAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - session: SessionContainerInterface; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | GeneralErrorResponse - >); - - appleRedirectHandlerPOST: - | undefined - | ((input: { code: string; state: string; options: ThirdPartyAPIOptions; userContext: any }) => Promise); -}; - -export type TypeThirdPartyEmailPasswordEmailDeliveryInput = TypeEmailPasswordEmailDeliveryInput; diff --git a/lib/ts/recipe/thirdpartyemailpassword/utils.ts b/lib/ts/recipe/thirdpartyemailpassword/utils.ts deleted file mode 100644 index 2c1577768..000000000 --- a/lib/ts/recipe/thirdpartyemailpassword/utils.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { NormalisedAppinfo } from "../../types"; -import { TypeInput, TypeNormalisedInput, TypeInputSignUp, TypeNormalisedInputSignUp } from "./types"; -import { NormalisedFormField } from "../emailpassword/types"; -import Recipe from "./recipe"; -import { normaliseSignUpFormFields } from "../emailpassword/utils"; -import { RecipeInterface, APIInterface } from "./types"; -import BackwardCompatibilityService from "./emaildelivery/services/backwardCompatibility"; -import { RecipeInterface as EPRecipeInterface } from "../emailpassword/types"; - -export function validateAndNormaliseUserInput( - recipeInstance: Recipe, - appInfo: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput { - let signUpFeature = validateAndNormaliseSignUpConfig( - recipeInstance, - appInfo, - config === undefined ? undefined : config.signUpFeature - ); - - let resetPasswordUsingTokenFeature = config === undefined ? undefined : config.resetPasswordUsingTokenFeature; - - let providers = config === undefined || config.providers === undefined ? [] : config.providers; - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - function getEmailDeliveryConfig(emailPasswordRecipeImpl: EPRecipeInterface, isInServerlessEnv: boolean) { - let emailService = config?.emailDelivery?.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation - */ - if (emailService === undefined) { - emailService = new BackwardCompatibilityService( - emailPasswordRecipeImpl, - appInfo, - isInServerlessEnv, - config?.resetPasswordUsingTokenFeature - ); - } - return { - ...config?.emailDelivery, - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }; - } - - return { - override, - getEmailDeliveryConfig, - signUpFeature, - providers, - resetPasswordUsingTokenFeature, - }; -} - -function validateAndNormaliseSignUpConfig( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInputSignUp -): TypeNormalisedInputSignUp { - let formFields: NormalisedFormField[] = normaliseSignUpFormFields( - config === undefined ? undefined : config.formFields - ); - - return { - formFields, - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/api/implementation.ts b/lib/ts/recipe/thirdpartypasswordless/api/implementation.ts deleted file mode 100644 index a7e47a52f..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/api/implementation.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { APIInterface } from "../types"; -import PasswordlessAPIImplementation from "../../passwordless/api/implementation"; -import ThirdPartyAPIImplementation from "../../thirdparty/api/implementation"; -import DerivedPwdless from "./passwordlessAPIImplementation"; -import DerivedTP from "./thirdPartyAPIImplementation"; - -export default function getAPIImplementation(): APIInterface { - let passwordlessImplementation = PasswordlessAPIImplementation(); - let thirdPartyImplementation = ThirdPartyAPIImplementation(); - return { - consumeCodePOST: passwordlessImplementation.consumeCodePOST?.bind(DerivedPwdless(this)), - createCodePOST: passwordlessImplementation.createCodePOST?.bind(DerivedPwdless(this)), - passwordlessUserEmailExistsGET: passwordlessImplementation.emailExistsGET?.bind(DerivedPwdless(this)), - passwordlessUserPhoneNumberExistsGET: passwordlessImplementation.phoneNumberExistsGET?.bind( - DerivedPwdless(this) - ), - resendCodePOST: passwordlessImplementation.resendCodePOST?.bind(DerivedPwdless(this)), - authorisationUrlGET: thirdPartyImplementation.authorisationUrlGET?.bind(DerivedTP(this)), - thirdPartySignInUpPOST: thirdPartyImplementation.signInUpPOST?.bind(DerivedTP(this)), - appleRedirectHandlerPOST: thirdPartyImplementation.appleRedirectHandlerPOST?.bind(DerivedTP(this)), - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.ts deleted file mode 100644 index 0e4c79a32..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { APIInterface } from "../../passwordless"; -import { APIInterface as ThirdPartyPasswordlessAPIInterface } from "../types"; - -export default function getIterfaceImpl(apiImplmentation: ThirdPartyPasswordlessAPIInterface): APIInterface { - return { - emailExistsGET: apiImplmentation.passwordlessUserEmailExistsGET?.bind(apiImplmentation), - consumeCodePOST: apiImplmentation.consumeCodePOST?.bind(apiImplmentation), - createCodePOST: apiImplmentation.createCodePOST?.bind(apiImplmentation), - phoneNumberExistsGET: apiImplmentation.passwordlessUserPhoneNumberExistsGET?.bind(apiImplmentation), - resendCodePOST: apiImplmentation.resendCodePOST?.bind(apiImplmentation), - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts deleted file mode 100644 index 99295860e..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { APIInterface } from "../../thirdparty"; -import { APIInterface as ThirdPartyPasswordlessAPIInterface } from "../types"; - -export default function getIterfaceImpl(apiImplmentation: ThirdPartyPasswordlessAPIInterface): APIInterface { - const signInUpPOSTFromThirdPartyPasswordless = apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation); - return { - authorisationUrlGET: apiImplmentation.authorisationUrlGET?.bind(apiImplmentation), - appleRedirectHandlerPOST: apiImplmentation.appleRedirectHandlerPOST?.bind(apiImplmentation), - signInUpPOST: - signInUpPOSTFromThirdPartyPasswordless === undefined - ? undefined - : async function (input) { - let result = await signInUpPOSTFromThirdPartyPasswordless(input); - if (result.status === "OK") { - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return { - ...result, - user: { - ...result.user, - thirdParty: { - ...result.user.thirdParty, - }, - }, - }; - } - return result; - }, - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts deleted file mode 100644 index 914128178..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; -import PasswordlessBackwardCompatibilityService from "../../../../passwordless/emaildelivery/services/backwardCompatibility"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; - -export default class BackwardCompatibilityService - implements EmailDeliveryInterface { - private passwordlessBackwardCompatibilityService: PasswordlessBackwardCompatibilityService; - - constructor( - appInfo: NormalisedAppinfo, - passwordlessFeature?: { - createAndSendCustomEmail?: ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - ) { - { - this.passwordlessBackwardCompatibilityService = new PasswordlessBackwardCompatibilityService( - appInfo, - passwordlessFeature?.createAndSendCustomEmail - ); - } - } - - sendEmail = async (input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any }) => { - await this.passwordlessBackwardCompatibilityService.sendEmail(input); - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/index.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/index.ts deleted file mode 100644 index 5d393e47d..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import SMTP from "./smtp"; -export let SMTPService = SMTP; diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts deleted file mode 100644 index f6e44b7ea..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../types"; -import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { createTransport } from "nodemailer"; -import OverrideableBuilder from "supertokens-js-override"; -import { getServiceImplementation } from "./serviceImplementation"; -import PasswordlessSMTPService from "../../../../passwordless/emaildelivery/services/smtp"; -import getPasswordlessServiceImplementation from "./serviceImplementation/passwordlessServiceImplementation"; - -export default class SMTPService implements EmailDeliveryInterface { - serviceImpl: ServiceInterface; - private passwordlessSMTPService: PasswordlessSMTPService; - - constructor(config: TypeInput) { - const transporter = createTransport({ - host: config.smtpSettings.host, - port: config.smtpSettings.port, - auth: { - user: config.smtpSettings.authUsername || config.smtpSettings.from.email, - pass: config.smtpSettings.password, - }, - secure: config.smtpSettings.secure, - }); - let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)); - if (config.override !== undefined) { - builder = builder.override(config.override); - } - this.serviceImpl = builder.build(); - - this.passwordlessSMTPService = new PasswordlessSMTPService({ - smtpSettings: config.smtpSettings, - override: (_) => { - return getPasswordlessServiceImplementation(this.serviceImpl); - }, - }); - } - - sendEmail = async (input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any }) => { - return await this.passwordlessSMTPService.sendEmail(input); - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts deleted file mode 100644 index 6cf7d5d81..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../../types"; -import { Transporter } from "nodemailer"; -import { - ServiceInterface, - TypeInputSendRawEmail, - GetContentResult, -} from "../../../../../../ingredients/emaildelivery/services/smtp"; -import { getServiceImplementation as getPasswordlessServiceImplementation } from "../../../../../passwordless/emaildelivery/services/smtp/serviceImplementation"; -import DerivedPwdless from "./passwordlessServiceImplementation"; - -export function getServiceImplementation( - transporter: Transporter, - from: { - name: string; - email: string; - } -): ServiceInterface { - let passwordlessServiceImpl = getPasswordlessServiceImplementation(transporter, from); - return { - sendRawEmail: async function (input: TypeInputSendRawEmail) { - await transporter.sendMail({ - from: `${from.name} <${from.email}>`, - to: input.toEmail, - subject: input.subject, - html: input.body, - }); - }, - getContent: async function ( - input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any } - ): Promise { - return await passwordlessServiceImpl.getContent.bind(DerivedPwdless(this))(input); - }, - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts deleted file mode 100644 index 1f0ac04bf..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../../types"; -import { - ServiceInterface, - TypeInputSendRawEmail, - GetContentResult, -} from "../../../../../../ingredients/emaildelivery/services/smtp"; -import { TypePasswordlessEmailDeliveryInput } from "../../../../../passwordless/types"; - -export default function getServiceInterface( - thirdpartyPasswordlessServiceImplementation: ServiceInterface -): ServiceInterface { - return { - sendRawEmail: async function (input: TypeInputSendRawEmail) { - return thirdpartyPasswordlessServiceImplementation.sendRawEmail(input); - }, - getContent: async function ( - input: TypePasswordlessEmailDeliveryInput & { userContext: any } - ): Promise { - return await thirdpartyPasswordlessServiceImplementation.getContent(input); - }, - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/error.ts b/lib/ts/recipe/thirdpartypasswordless/error.ts deleted file mode 100644 index a55e9656c..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/error.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; - -export default class ThirdPartyEmailPasswordError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }) { - super({ - ...options, - }); - this.fromRecipe = "thirdpartypasswordless"; - } -} diff --git a/lib/ts/recipe/thirdpartypasswordless/index.ts b/lib/ts/recipe/thirdpartypasswordless/index.ts deleted file mode 100644 index 3423ddcde..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/index.ts +++ /dev/null @@ -1,281 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import Recipe from "./recipe"; -import SuperTokensError from "./error"; -import * as thirdPartyProviders from "../thirdparty/providers"; -import { - RecipeInterface, - User, - APIInterface, - PasswordlessAPIOptions, - ThirdPartyAPIOptions, - TypeThirdPartyPasswordlessEmailDeliveryInput, -} from "./types"; -import { TypeProvider } from "../thirdparty/types"; -import { TypePasswordlessSmsDeliveryInput } from "../passwordless/types"; - -export default class Wrapper { - static init = Recipe.init; - - static Error = SuperTokensError; - - static thirdPartySignInUp(thirdPartyId: string, thirdPartyUserId: string, email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartySignInUp({ - thirdPartyId, - thirdPartyUserId, - email, - userContext, - }); - } - - static getUserByThirdPartyInfo(thirdPartyId: string, thirdPartyUserId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ - thirdPartyId, - thirdPartyUserId, - userContext, - }); - } - - static getUserById(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - - static getUsersByEmail(email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } - - static createCode( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { userInputCode?: string; userContext?: any } - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ - userContext: {}, - ...input, - }); - } - - static createNewCodeForDevice(input: { deviceId: string; userInputCode?: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ - userContext: {}, - ...input, - }); - } - - static consumeCode( - input: - | { - preAuthSessionId: string; - userInputCode: string; - deviceId: string; - userContext?: any; - } - | { - preAuthSessionId: string; - linkCode: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ userContext: {}, ...input }); - } - - static getUserByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByPhoneNumber({ userContext: {}, ...input }); - } - - static updatePasswordlessUser(input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext?: any; - }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updatePasswordlessUser({ - userContext: {}, - ...input, - }); - } - - static revokeAllCodes( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ userContext: {}, ...input }); - } - - static revokeCode(input: { codeId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ userContext: {}, ...input }); - } - - static listCodesByEmail(input: { email: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ userContext: {}, ...input }); - } - - static listCodesByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ - userContext: {}, - ...input, - }); - } - - static listCodesByDeviceId(input: { deviceId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ userContext: {}, ...input }); - } - - static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; userContext?: any }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ - userContext: {}, - ...input, - }); - } - - static createMagicLink( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().passwordlessRecipe.createMagicLink({ userContext: {}, ...input }); - } - - static passwordlessSignInUp( - input: - | { - email: string; - userContext?: any; - } - | { - phoneNumber: string; - userContext?: any; - } - ) { - return Recipe.getInstanceOrThrowError().passwordlessRecipe.signInUp({ userContext: {}, ...input }); - } - - static Google = thirdPartyProviders.Google; - - static Github = thirdPartyProviders.Github; - - static Facebook = thirdPartyProviders.Facebook; - - static Apple = thirdPartyProviders.Apple; - - static Discord = thirdPartyProviders.Discord; - - static GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces; - - static Bitbucket = thirdPartyProviders.Bitbucket; - - static GitLab = thirdPartyProviders.GitLab; - - // static Okta = thirdPartyProviders.Okta; - - // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; - - static async sendEmail(input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext?: any }) { - return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, - ...input, - }); - } - - static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { - return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ - userContext: {}, - ...input, - }); - } -} - -export let init = Wrapper.init; - -export let Error = Wrapper.Error; - -export let thirdPartySignInUp = Wrapper.thirdPartySignInUp; - -export let passwordlessSignInUp = Wrapper.passwordlessSignInUp; - -export let getUserById = Wrapper.getUserById; - -export let getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; - -export let getUsersByEmail = Wrapper.getUsersByEmail; - -export let createCode = Wrapper.createCode; - -export let consumeCode = Wrapper.consumeCode; - -export let getUserByPhoneNumber = Wrapper.getUserByPhoneNumber; - -export let listCodesByDeviceId = Wrapper.listCodesByDeviceId; - -export let listCodesByEmail = Wrapper.listCodesByEmail; - -export let listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber; - -export let listCodesByPreAuthSessionId = Wrapper.listCodesByPreAuthSessionId; - -export let createNewCodeForDevice = Wrapper.createNewCodeForDevice; - -export let updatePasswordlessUser = Wrapper.updatePasswordlessUser; - -export let revokeAllCodes = Wrapper.revokeAllCodes; - -export let revokeCode = Wrapper.revokeCode; - -export let createMagicLink = Wrapper.createMagicLink; - -export let Google = Wrapper.Google; - -export let Github = Wrapper.Github; - -export let Facebook = Wrapper.Facebook; - -export let Apple = Wrapper.Apple; - -export let Discord = Wrapper.Discord; - -export let GoogleWorkspaces = Wrapper.GoogleWorkspaces; - -export let Bitbucket = Wrapper.Bitbucket; - -export let GitLab = Wrapper.GitLab; - -// export let Okta = Wrapper.Okta; - -// export let ActiveDirectory = Wrapper.ActiveDirectory; - -export type { RecipeInterface, TypeProvider, User, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions }; - -export let sendEmail = Wrapper.sendEmail; - -export let sendSms = Wrapper.sendSms; diff --git a/lib/ts/recipe/thirdpartypasswordless/recipe.ts b/lib/ts/recipe/thirdpartypasswordless/recipe.ts deleted file mode 100644 index 2221312d2..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/recipe.ts +++ /dev/null @@ -1,261 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; -import PasswordlessRecipe from "../passwordless/recipe"; -import ThirdPartyRecipe from "../thirdparty/recipe"; -import { BaseRequest, BaseResponse } from "../../framework"; -import STError from "./error"; -import { - TypeInput, - TypeNormalisedInput, - RecipeInterface, - APIInterface, - TypeThirdPartyPasswordlessEmailDeliveryInput, - TypeThirdPartyPasswordlessSmsDeliveryInput, -} from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import STErrorPasswordless from "../passwordless/error"; -import STErrorThirdParty from "../thirdparty/error"; -import NormalisedURLPath from "../../normalisedURLPath"; -import RecipeImplementation from "./recipeImplementation"; -import PasswordlessRecipeImplementation from "./recipeImplementation/passwordlessRecipeImplementation"; -import ThirdPartyRecipeImplementation from "./recipeImplementation/thirdPartyRecipeImplementation"; -import getThirdPartyIterfaceImpl from "./api/thirdPartyAPIImplementation"; -import getPasswordlessInterfaceImpl from "./api/passwordlessAPIImplementation"; -import APIImplementation from "./api/implementation"; -import { Querier } from "../../querier"; -import OverrideableBuilder from "supertokens-js-override"; -import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; - -export default class Recipe extends RecipeModule { - private static instance: Recipe | undefined = undefined; - static RECIPE_ID = "thirdpartypasswordless"; - - config: TypeNormalisedInput; - - passwordlessRecipe: PasswordlessRecipe; - - private thirdPartyRecipe: ThirdPartyRecipe | undefined; - - recipeInterfaceImpl: RecipeInterface; - - apiImpl: APIInterface; - - emailDelivery: EmailDeliveryIngredient; - - smsDelivery: SmsDeliveryIngredient; - - isInServerlessEnv: boolean; - - constructor( - recipeId: string, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - config: TypeInput, - recipes: { - thirdPartyInstance: ThirdPartyRecipe | undefined; - passwordlessInstance: PasswordlessRecipe | undefined; - }, - ingredients: { - emailDelivery: EmailDeliveryIngredient | undefined; - smsDelivery: SmsDeliveryIngredient | undefined; - } - ) { - super(recipeId, appInfo); - this.isInServerlessEnv = isInServerlessEnv; - this.config = validateAndNormaliseUserInput(appInfo, config); - - { - let builder = new OverrideableBuilder( - RecipeImplementation( - Querier.getNewInstanceOrThrowError(PasswordlessRecipe.RECIPE_ID), - Querier.getNewInstanceOrThrowError(ThirdPartyRecipe.RECIPE_ID) - ) - ); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - { - let builder = new OverrideableBuilder(APIImplementation()); - this.apiImpl = builder.override(this.config.override.apis).build(); - } - - this.emailDelivery = - ingredients.emailDelivery === undefined - ? new EmailDeliveryIngredient( - this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv) - ) - : ingredients.emailDelivery; - - this.smsDelivery = - ingredients.smsDelivery === undefined - ? new SmsDeliveryIngredient(this.config.getSmsDeliveryConfig()) - : ingredients.smsDelivery; - - this.passwordlessRecipe = - recipes.passwordlessInstance !== undefined - ? recipes.passwordlessInstance - : new PasswordlessRecipe( - recipeId, - appInfo, - isInServerlessEnv, - { - ...this.config, - override: { - functions: (_) => { - return PasswordlessRecipeImplementation(this.recipeInterfaceImpl); - }, - apis: (_) => { - return getPasswordlessInterfaceImpl(this.apiImpl); - }, - }, - }, - { - emailDelivery: this.emailDelivery, - smsDelivery: this.smsDelivery, - } - ); - - if (this.config.providers.length !== 0) { - this.thirdPartyRecipe = - recipes.thirdPartyInstance !== undefined - ? recipes.thirdPartyInstance - : new ThirdPartyRecipe( - recipeId, - appInfo, - isInServerlessEnv, - { - override: { - functions: (_) => { - return ThirdPartyRecipeImplementation(this.recipeInterfaceImpl); - }, - apis: (_) => { - return getThirdPartyIterfaceImpl(this.apiImpl); - }, - }, - signInAndUpFeature: { - providers: this.config.providers, - }, - }, - {}, - { - emailDelivery: this.emailDelivery, - } - ); - } - } - - static init(config: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe( - Recipe.RECIPE_ID, - appInfo, - isInServerlessEnv, - config, - { - passwordlessInstance: undefined, - thirdPartyInstance: undefined, - }, - { - emailDelivery: undefined, - smsDelivery: undefined, - } - ); - return Recipe.instance; - } else { - throw new Error( - "ThirdPartyPasswordless recipe has already been initialised. Please check your code for bugs." - ); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - getAPIsHandled = (): APIHandled[] => { - let apisHandled = [...this.passwordlessRecipe.getAPIsHandled()]; - if (this.thirdPartyRecipe !== undefined) { - apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()); - } - return apisHandled; - }; - - handleAPIRequest = async ( - id: string, - req: BaseRequest, - res: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ): Promise => { - if (this.passwordlessRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined) { - return await this.passwordlessRecipe.handleAPIRequest(id, req, res, path, method); - } - if ( - this.thirdPartyRecipe !== undefined && - this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined - ) { - return await this.thirdPartyRecipe.handleAPIRequest(id, req, res, path, method); - } - return false; - }; - - handleError = async ( - err: STErrorPasswordless | STErrorThirdParty, - request: BaseRequest, - response: BaseResponse - ): Promise => { - if (err.fromRecipe === Recipe.RECIPE_ID) { - throw err; - } else { - if (this.passwordlessRecipe.isErrorFromThisRecipe(err)) { - return await this.passwordlessRecipe.handleError(err, request, response); - } else if (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err)) { - return await this.thirdPartyRecipe.handleError(err, request, response); - } - throw err; - } - }; - - getAllCORSHeaders = (): string[] => { - let corsHeaders = [...this.passwordlessRecipe.getAllCORSHeaders()]; - if (this.thirdPartyRecipe !== undefined) { - corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()); - } - return corsHeaders; - }; - - isErrorFromThisRecipe = (err: any): err is STError => { - return ( - STError.isErrorFromSuperTokens(err) && - (err.fromRecipe === Recipe.RECIPE_ID || - this.passwordlessRecipe.isErrorFromThisRecipe(err) || - (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err))) - ); - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts deleted file mode 100644 index caad52c5f..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { RecipeInterface, User } from "../types"; -import PasswordlessImplemenation from "../../passwordless/recipeImplementation"; - -import ThirdPartyImplemenation from "../../thirdparty/recipeImplementation"; -import { RecipeInterface as ThirdPartyRecipeInterface } from "../../thirdparty"; -import { Querier } from "../../../querier"; -import DerivedPwdless from "./passwordlessRecipeImplementation"; -import DerivedTP from "./thirdPartyRecipeImplementation"; - -export default function getRecipeInterface(passwordlessQuerier: Querier, thirdPartyQuerier?: Querier): RecipeInterface { - let originalPasswordlessImplementation = PasswordlessImplemenation(passwordlessQuerier); - let originalThirdPartyImplementation: undefined | ThirdPartyRecipeInterface; - if (thirdPartyQuerier !== undefined) { - originalThirdPartyImplementation = ThirdPartyImplemenation(thirdPartyQuerier); - } - - return { - consumeCode: async function (input) { - return originalPasswordlessImplementation.consumeCode.bind(DerivedPwdless(this))(input); - }, - createCode: async function (input) { - return originalPasswordlessImplementation.createCode.bind(DerivedPwdless(this))(input); - }, - createNewCodeForDevice: async function (input) { - return originalPasswordlessImplementation.createNewCodeForDevice.bind(DerivedPwdless(this))(input); - }, - getUserByPhoneNumber: async function (input) { - return originalPasswordlessImplementation.getUserByPhoneNumber.bind(DerivedPwdless(this))(input); - }, - listCodesByDeviceId: async function (input) { - return originalPasswordlessImplementation.listCodesByDeviceId.bind(DerivedPwdless(this))(input); - }, - listCodesByEmail: async function (input) { - return originalPasswordlessImplementation.listCodesByEmail.bind(DerivedPwdless(this))(input); - }, - listCodesByPhoneNumber: async function (input) { - return originalPasswordlessImplementation.listCodesByPhoneNumber.bind(DerivedPwdless(this))(input); - }, - listCodesByPreAuthSessionId: async function (input) { - return originalPasswordlessImplementation.listCodesByPreAuthSessionId.bind(DerivedPwdless(this))(input); - }, - revokeAllCodes: async function (input) { - return originalPasswordlessImplementation.revokeAllCodes.bind(DerivedPwdless(this))(input); - }, - revokeCode: async function (input) { - return originalPasswordlessImplementation.revokeCode.bind(DerivedPwdless(this))(input); - }, - - updatePasswordlessUser: async function (this: RecipeInterface, input) { - let user = await this.getUserById({ userId: input.userId, userContext: input.userContext }); - if (user === undefined) { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } else if ("thirdParty" in user) { - throw new Error( - "Cannot update passwordless user info for those who signed up using third party login." - ); - } - return originalPasswordlessImplementation.updateUser.bind(DerivedPwdless(this))(input); - }, - - thirdPartySignInUp: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { - if (originalThirdPartyImplementation === undefined) { - throw new Error("No thirdparty provider configured"); - } - return originalThirdPartyImplementation.signInUp.bind(DerivedTP(this))(input); - }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user: User | undefined = await originalPasswordlessImplementation.getUserById.bind( - DerivedPwdless(this) - )(input); - if (user !== undefined) { - return user; - } - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return await originalThirdPartyImplementation.getUserById.bind(DerivedTP(this))(input); - }, - - getUsersByEmail: async function ({ email, userContext }: { email: string; userContext: any }): Promise { - let userFromEmailPass: User | undefined = await originalPasswordlessImplementation.getUserByEmail.bind( - DerivedPwdless(this) - )({ email, userContext }); - - if (originalThirdPartyImplementation === undefined) { - return userFromEmailPass === undefined ? [] : [userFromEmailPass]; - } - let usersFromThirdParty: User[] = await originalThirdPartyImplementation.getUsersByEmail.bind( - DerivedTP(this) - )({ email, userContext }); - - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }, - - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise { - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind(DerivedTP(this))(input); - }, - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts deleted file mode 100644 index 676384fc8..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { RecipeInterface } from "../../passwordless/types"; -import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from "../types"; - -export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface { - return { - consumeCode: async function (input) { - return await recipeInterface.consumeCode(input); - }, - createCode: async function (input) { - return await recipeInterface.createCode(input); - }, - createNewCodeForDevice: async function (input) { - return await recipeInterface.createNewCodeForDevice(input); - }, - getUserByEmail: async function (input) { - let users = await recipeInterface.getUsersByEmail(input); - for (let i = 0; i < users.length; i++) { - let u = users[i]; - if (!("thirdParty" in u)) { - return u; - } - } - return undefined; - }, - getUserById: async function (input) { - let user = await recipeInterface.getUserById(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }, - getUserByPhoneNumber: async function (input) { - let user = await recipeInterface.getUserByPhoneNumber(input); - if (user !== undefined && "thirdParty" in user) { - // this is a thirdparty user. - return undefined; - } - return user; - }, - listCodesByDeviceId: async function (input) { - return await recipeInterface.listCodesByDeviceId(input); - }, - listCodesByEmail: async function (input) { - return await recipeInterface.listCodesByEmail(input); - }, - listCodesByPhoneNumber: async function (input) { - return await recipeInterface.listCodesByPhoneNumber(input); - }, - listCodesByPreAuthSessionId: async function (input) { - return await recipeInterface.listCodesByPreAuthSessionId(input); - }, - revokeAllCodes: async function (input) { - return await recipeInterface.revokeAllCodes(input); - }, - revokeCode: async function (input) { - return await recipeInterface.revokeCode(input); - }, - updateUser: async function (input) { - return await recipeInterface.updatePasswordlessUser(input); - }, - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts deleted file mode 100644 index 78513e90c..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { RecipeInterface, User } from "../../thirdparty/types"; -import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from "../types"; - -export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface { - return { - getUserByThirdPartyInfo: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise { - let user = await recipeInterface.getUserByThirdPartyInfo(input); - if (user === undefined || !("thirdParty" in user)) { - return undefined; - } - return user; - }, - - signInUp: async function (input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }> { - let result = await recipeInterface.thirdPartySignInUp(input); - if (!("thirdParty" in result.user)) { - throw new Error("Should never come here"); - } - return { - status: "OK", - createdNewUser: result.createdNewUser, - user: result.user, - }; - }, - - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || !("thirdParty" in user)) { - // either user is undefined or it's an email password user. - return undefined; - } - return user; - }, - - getUsersByEmail: async function (input: { email: string; userContext: any }): Promise { - let users = await recipeInterface.getUsersByEmail(input); - - // we filter out all non thirdparty users. - return users.filter((u) => { - return "thirdParty" in u; - }) as User[]; - }, - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts deleted file mode 100644 index ddffd8a65..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { NormalisedAppinfo } from "../../../../../types"; -import PasswordlessBackwardCompatibilityService from "../../../../passwordless/smsdelivery/services/backwardCompatibility"; - -export default class BackwardCompatibilityService - implements SmsDeliveryInterface { - private passwordlessBackwardCompatibilityService: PasswordlessBackwardCompatibilityService; - - constructor( - appInfo: NormalisedAppinfo, - passwordlessFeature?: { - createAndSendCustomTextMessage?: ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - ) { - this.passwordlessBackwardCompatibilityService = new PasswordlessBackwardCompatibilityService( - appInfo, - passwordlessFeature?.createAndSendCustomTextMessage - ); - } - - sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: any }) => { - await this.passwordlessBackwardCompatibilityService.sendSms(input); - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/index.ts b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/index.ts deleted file mode 100644 index 882d9afeb..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import Twilio from "./twilio"; -import Supertokens from "./supertokens"; - -export let TwilioService = Twilio; -export let SupertokensService = Supertokens; diff --git a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts deleted file mode 100644 index 303df7884..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; -import PasswordlessSupertokensService from "../../../../passwordless/smsdelivery/services/supertokens"; - -export default class SupertokensService implements SmsDeliveryInterface { - private passwordlessSupertokensService: PasswordlessSupertokensService; - - constructor(apiKey: string) { - this.passwordlessSupertokensService = new PasswordlessSupertokensService(apiKey); - } - - sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: any }) => { - await this.passwordlessSupertokensService.sendSms(input); - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts deleted file mode 100644 index bdf226d37..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeInput } from "../../../../../ingredients/smsdelivery/services/twilio"; -import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; -import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; -import PasswordlessTwilioService from "../../../../passwordless/smsdelivery/services/twilio/index"; - -export default class TwilioService implements SmsDeliveryInterface { - private passwordlessTwilioService: PasswordlessTwilioService; - - constructor(config: TypeInput) { - this.passwordlessTwilioService = new PasswordlessTwilioService(config); - } - - sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: any }) => { - await this.passwordlessTwilioService.sendSms(input); - }; -} diff --git a/lib/ts/recipe/thirdpartypasswordless/types.ts b/lib/ts/recipe/thirdpartypasswordless/types.ts deleted file mode 100644 index 955914642..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/types.ts +++ /dev/null @@ -1,465 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -import { TypeProvider, APIOptions as ThirdPartyAPIOptionsOriginal } from "../thirdparty/types"; -import { - DeviceType as DeviceTypeOriginal, - APIOptions as PasswordlessAPIOptionsOriginal, - TypePasswordlessEmailDeliveryInput, - TypePasswordlessSmsDeliveryInput, -} from "../passwordless/types"; -import OverrideableBuilder from "supertokens-js-override"; -import { SessionContainerInterface } from "../session/types"; -import { - TypeInput as EmailDeliveryTypeInput, - TypeInputWithService as EmailDeliveryTypeInputWithService, -} from "../../ingredients/emaildelivery/types"; -import { - TypeInput as SmsDeliveryTypeInput, - TypeInputWithService as SmsDeliveryTypeInputWithService, -} from "../../ingredients/smsdelivery/types"; -import { GeneralErrorResponse } from "../../types"; - -export type DeviceType = DeviceTypeOriginal; - -export type User = ( - | { - // passwordless user properties - email?: string; - phoneNumber?: string; - } - | { - // third party user properties - email: string; - thirdParty: { - id: string; - userId: string; - }; - } -) & { - id: string; - timeJoined: number; -}; - -export type TypeInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: ( - input: { - // Where the message should be delivered. - email: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - - // Override to use custom template/contact method - /** - * @deprecated Please use smsDelivery config instead - */ - createAndSendCustomTextMessage?: ( - input: { - // Where the message should be delivered. - phoneNumber: string; - // This has to be entered on the starting device to finish sign in/up - userInputCode?: string; - // Full url that the end-user can click to finish sign in/up - urlWithLinkCode?: string; - codeLifetime: number; - // Unlikely, but someone could display this (or a derived thing) to identify the device - preAuthSessionId: string; - }, - userContext: any - ) => Promise; - } -) & { - /** - * Unlike passwordless recipe, emailDelivery config is outside here because regardless - * of `contactMethod` value, the config is required for email verification recipe - */ - emailDelivery?: EmailDeliveryTypeInput; - smsDelivery?: SmsDeliveryTypeInput; - providers?: TypeProvider[]; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - - // Override this to override how user input codes are generated - // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (userContext: any) => Promise | string; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = ( - | { - contactMethod: "PHONE"; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - } - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress?: (email: string) => Promise | string | undefined; - validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined; - } -) & { - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - - // Override this to override how user input codes are generated - // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (userContext: any) => Promise | string; - providers: TypeProvider[]; - getEmailDeliveryConfig: ( - recipeImpl: RecipeInterface, - isInServerlessEnv: boolean - ) => EmailDeliveryTypeInputWithService; - getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - - getUsersByEmail(input: { email: string; userContext: any }): Promise; - - getUserByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - - getUserByThirdPartyInfo(input: { - thirdPartyId: string; - thirdPartyUserId: string; - userContext: any; - }): Promise; - - thirdPartySignInUp(input: { - thirdPartyId: string; - thirdPartyUserId: string; - email: string; - userContext: any; - }): Promise<{ status: "OK"; createdNewUser: boolean; user: User }>; - - createCode: ( - input: ( - | { - email: string; - } - | { - phoneNumber: string; - } - ) & { userInputCode?: string; userContext: any } - ) => Promise<{ - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - }>; - - createNewCodeForDevice: (input: { - deviceId: string; - userInputCode?: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - } - | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" } - >; - - consumeCode: ( - input: - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - userContext: any; - } - | { - linkCode: string; - preAuthSessionId: string; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | { status: "RESTART_FLOW_ERROR" } - >; - - updatePasswordlessUser: (input: { - userId: string; - email?: string | null; - phoneNumber?: string | null; - userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR"; - }>; - - revokeAllCodes: ( - input: - | { - email: string; - userContext: any; - } - | { - phoneNumber: string; - userContext: any; - } - ) => Promise<{ - status: "OK"; - }>; - - revokeCode: (input: { - codeId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - }>; - - listCodesByEmail: (input: { email: string; userContext: any }) => Promise; - - listCodesByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise; - - listCodesByDeviceId: (input: { deviceId: string; userContext: any }) => Promise; - - listCodesByPreAuthSessionId: (input: { - preAuthSessionId: string; - userContext: any; - }) => Promise; -}; - -export type PasswordlessAPIOptions = PasswordlessAPIOptionsOriginal; - -export type ThirdPartyAPIOptions = ThirdPartyAPIOptionsOriginal; -export type APIInterface = { - authorisationUrlGET: - | undefined - | ((input: { - provider: TypeProvider; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - url: string; - } - | GeneralErrorResponse - >); - - thirdPartySignInUpPOST: - | undefined - | ((input: { - provider: TypeProvider; - code: string; - redirectURI: string; - authCodeResponse?: any; - clientId?: string; - options: ThirdPartyAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - authCodeResponse: any; - } - | GeneralErrorResponse - | { - status: "NO_EMAIL_GIVEN_BY_PROVIDER"; - } - >); - - appleRedirectHandlerPOST: - | undefined - | ((input: { code: string; state: string; options: ThirdPartyAPIOptions; userContext: any }) => Promise); - - createCodePOST: - | undefined - | (( - input: ({ email: string } | { phoneNumber: string }) & { - options: PasswordlessAPIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - deviceId: string; - preAuthSessionId: string; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - } - | GeneralErrorResponse - >); - - resendCodePOST: - | undefined - | (( - input: { deviceId: string; preAuthSessionId: string } & { - options: PasswordlessAPIOptions; - userContext: any; - } - ) => Promise); - - consumeCodePOST: - | undefined - | (( - input: ( - | { - userInputCode: string; - deviceId: string; - preAuthSessionId: string; - } - | { - linkCode: string; - preAuthSessionId: string; - } - ) & { - options: PasswordlessAPIOptions; - userContext: any; - } - ) => Promise< - | { - status: "OK"; - createdNewUser: boolean; - user: User; - session: SessionContainerInterface; - } - | { - status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; - failedCodeInputAttemptCount: number; - maximumCodeInputAttempts: number; - } - | GeneralErrorResponse - | { status: "RESTART_FLOW_ERROR" } - >); - - passwordlessUserEmailExistsGET: - | undefined - | ((input: { - email: string; - options: PasswordlessAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); - - passwordlessUserPhoneNumberExistsGET: - | undefined - | ((input: { - phoneNumber: string; - options: PasswordlessAPIOptions; - userContext: any; - }) => Promise< - | { - status: "OK"; - exists: boolean; - } - | GeneralErrorResponse - >); -}; - -export type TypeThirdPartyPasswordlessEmailDeliveryInput = TypePasswordlessEmailDeliveryInput; - -export type TypeThirdPartyPasswordlessSmsDeliveryInput = TypePasswordlessSmsDeliveryInput; diff --git a/lib/ts/recipe/thirdpartypasswordless/utils.ts b/lib/ts/recipe/thirdpartypasswordless/utils.ts deleted file mode 100644 index b68a8eef5..000000000 --- a/lib/ts/recipe/thirdpartypasswordless/utils.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { NormalisedAppinfo } from "../../types"; -import { TypeInput, TypeNormalisedInput } from "./types"; -import { RecipeInterface, APIInterface } from "./types"; -import BackwardCompatibilityEmailService from "./emaildelivery/services/backwardCompatibility"; -import BackwardCompatibilitySmsService from "./smsdelivery/services/backwardCompatibility"; - -export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { - let providers = config.providers === undefined ? [] : config.providers; - - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - function getEmailDeliveryConfig() { - let emailService = config?.emailDelivery?.service; - /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default - * createAndSendCustomEmail implementation - */ - if (emailService === undefined) { - emailService = new BackwardCompatibilityEmailService(appInfo, { - createAndSendCustomEmail: - config?.contactMethod !== "PHONE" ? config?.createAndSendCustomEmail : undefined, - }); - } - return { - ...config?.emailDelivery, - /** - * if we do - * let emailDelivery = { - * service: emailService, - * ...config.emailDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: emailService, - }; - } - - function getSmsDeliveryConfig() { - let smsService = config?.smsDelivery?.service; - /** - * following code is for backward compatibility. - * if user has not passed smsDelivery config, we - * use the createAndSendCustomTextMessage config. If the user - * has not passed even that config, we use the default - * createAndSendCustomTextMessage implementation - */ - if (smsService === undefined) { - smsService = new BackwardCompatibilitySmsService(appInfo, { - createAndSendCustomTextMessage: - config?.contactMethod !== "EMAIL" ? config?.createAndSendCustomTextMessage : undefined, - }); - } - return { - ...config?.smsDelivery, - /** - * if we do - * let smsDelivery = { - * service: smsService, - * ...config.smsDelivery, - * }; - * - * and if the user has passed service as undefined, - * it it again get set to undefined, so we - * set service at the end - */ - service: smsService, - }; - } - - return { - ...config, - providers, - override, - getEmailDeliveryConfig, - getSmsDeliveryConfig, - }; -} diff --git a/lib/ts/recipe/usermetadata/index.ts b/lib/ts/recipe/usermetadata/index.ts deleted file mode 100644 index 5148c96da..000000000 --- a/lib/ts/recipe/usermetadata/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { JSONObject } from "../../types"; -import Recipe from "./recipe"; -import { RecipeInterface } from "./types"; - -export default class Wrapper { - static init = Recipe.init; - - static async getUserMetadata(userId: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserMetadata({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async updateUserMetadata(userId: string, metadataUpdate: JSONObject, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateUserMetadata({ - userId, - metadataUpdate, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async clearUserMetadata(userId: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.clearUserMetadata({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } -} - -export const init = Wrapper.init; -export const getUserMetadata = Wrapper.getUserMetadata; -export const updateUserMetadata = Wrapper.updateUserMetadata; -export const clearUserMetadata = Wrapper.clearUserMetadata; - -export type { RecipeInterface, JSONObject }; diff --git a/lib/ts/recipe/usermetadata/recipe.ts b/lib/ts/recipe/usermetadata/recipe.ts deleted file mode 100644 index 1386084de..000000000 --- a/lib/ts/recipe/usermetadata/recipe.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import SuperTokensError from "../../error"; -import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import normalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; - -import RecipeImplementation from "./recipeImplementation"; -import { RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import OverrideableBuilder from "supertokens-js-override"; - -export default class Recipe extends RecipeModule { - static RECIPE_ID = "usermetadata"; - private static instance: Recipe | undefined = undefined; - - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - isInServerlessEnv: boolean; - - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - - { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - } - - /* Init functions */ - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error( - "Initialisation not done. Did you forget to call the UserMetadata.init or SuperTokens.init function?" - ); - } - - static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("UserMetadata recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - /* RecipeModule functions */ - - getAPIsHandled(): APIHandled[] { - return []; - } - - // This stub is required to implement RecipeModule - handleAPIRequest = async ( - _: string, - __: BaseRequest, - ___: BaseResponse, - ____: normalisedURLPath, - _____: HTTPMethod - ): Promise => { - throw new Error("Should never come here"); - }; - - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise { - throw error; - } - - getAllCORSHeaders(): string[] { - return []; - } - - isErrorFromThisRecipe(err: any): err is error { - return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - } -} diff --git a/lib/ts/recipe/usermetadata/recipeImplementation.ts b/lib/ts/recipe/usermetadata/recipeImplementation.ts deleted file mode 100644 index 34ff4c26c..000000000 --- a/lib/ts/recipe/usermetadata/recipeImplementation.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { RecipeInterface } from "."; -import NormalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; - -export default function getRecipeInterface(querier: Querier): RecipeInterface { - return { - getUserMetadata: function ({ userId }) { - return querier.sendGetRequest(new NormalisedURLPath("/recipe/user/metadata"), { userId }); - }, - - updateUserMetadata: function ({ userId, metadataUpdate }) { - return querier.sendPutRequest(new NormalisedURLPath("/recipe/user/metadata"), { - userId, - metadataUpdate, - }); - }, - - clearUserMetadata: function ({ userId }) { - return querier.sendPostRequest(new NormalisedURLPath("/recipe/user/metadata/remove"), { - userId, - }); - }, - }; -} diff --git a/lib/ts/recipe/usermetadata/types.ts b/lib/ts/recipe/usermetadata/types.ts deleted file mode 100644 index fc604a2e2..000000000 --- a/lib/ts/recipe/usermetadata/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import OverrideableBuilder from "supertokens-js-override"; -import { JSONObject } from "../../types"; - -export type TypeInput = { - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type APIInterface = {}; - -export type RecipeInterface = { - getUserMetadata: (input: { - userId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - metadata: any; - }>; - - /** - * Updates the metadata object of the user by doing a shallow merge of the stored and the update JSONs - * and removing properties set to null on the root level of the update object. - * e.g.: - * - stored: `{ "preferences": { "theme":"dark" }, "notifications": { "email": true }, "todos": ["example"] }` - * - update: `{ "notifications": { "sms": true }, "todos": null }` - * - result: `{ "preferences": { "theme":"dark" }, "notifications": { "sms": true } }` - */ - updateUserMetadata: (input: { - userId: string; - metadataUpdate: JSONObject; - userContext: any; - }) => Promise<{ - status: "OK"; - metadata: JSONObject; - }>; - - clearUserMetadata: (input: { - userId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - }>; -}; diff --git a/lib/ts/recipe/usermetadata/utils.ts b/lib/ts/recipe/usermetadata/utils.ts deleted file mode 100644 index 02b2265b2..000000000 --- a/lib/ts/recipe/usermetadata/utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { NormalisedAppinfo } from "../../types"; -import Recipe from "./recipe"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; - -export function validateAndNormaliseUserInput( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput { - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - return { - override, - }; -} diff --git a/lib/ts/recipe/userroles/index.ts b/lib/ts/recipe/userroles/index.ts deleted file mode 100644 index 33262752b..000000000 --- a/lib/ts/recipe/userroles/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { PermissionClaim } from "./permissionClaim"; -import Recipe from "./recipe"; -import { RecipeInterface } from "./types"; -import { UserRoleClaim } from "./userRoleClaim"; - -export default class Wrapper { - static init = Recipe.init; - static PermissionClaim = PermissionClaim; - static UserRoleClaim = UserRoleClaim; - - static async addRoleToUser(userId: string, role: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.addRoleToUser({ - userId, - role, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async removeUserRole(userId: string, role: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removeUserRole({ - userId, - role, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async getRolesForUser(userId: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getRolesForUser({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async getUsersThatHaveRole(role: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersThatHaveRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewRoleOrAddPermissions({ - role, - permissions, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async getPermissionsForRole(role: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getPermissionsForRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async removePermissionsFromRole(role: string, permissions: string[], userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removePermissionsFromRole({ - role, - permissions, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async getRolesThatHavePermission(permission: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getRolesThatHavePermission({ - permission, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async deleteRole(role: string, userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.deleteRole({ - role, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static async getAllRoles(userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getAllRoles({ - userContext: userContext === undefined ? {} : userContext, - }); - } -} - -export const init = Wrapper.init; -export const addRoleToUser = Wrapper.addRoleToUser; -export const removeUserRole = Wrapper.removeUserRole; -export const getRolesForUser = Wrapper.getRolesForUser; -export const getUsersThatHaveRole = Wrapper.getUsersThatHaveRole; -export const createNewRoleOrAddPermissions = Wrapper.createNewRoleOrAddPermissions; -export const getPermissionsForRole = Wrapper.getPermissionsForRole; -export const removePermissionsFromRole = Wrapper.removePermissionsFromRole; -export const getRolesThatHavePermission = Wrapper.getRolesThatHavePermission; -export const deleteRole = Wrapper.deleteRole; -export const getAllRoles = Wrapper.getAllRoles; -export { UserRoleClaim } from "./userRoleClaim"; -export { PermissionClaim } from "./permissionClaim"; - -export type { RecipeInterface }; diff --git a/lib/ts/recipe/userroles/permissionClaim.ts b/lib/ts/recipe/userroles/permissionClaim.ts deleted file mode 100644 index 341810ed7..000000000 --- a/lib/ts/recipe/userroles/permissionClaim.ts +++ /dev/null @@ -1,41 +0,0 @@ -import UserRoleRecipe from "./recipe"; -import { PrimitiveArrayClaim } from "../session/claimBaseClasses/primitiveArrayClaim"; - -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -export class PermissionClaimClass extends PrimitiveArrayClaim { - constructor() { - super({ - key: "st-perm", - async fetchValue(userId, userContext) { - const recipe = UserRoleRecipe.getInstanceOrThrowError(); - - // We fetch the roles because the rolesClaim may not be present in the payload - const userRoles = await recipe.recipeInterfaceImpl.getRolesForUser({ - userId, - userContext, - }); - - // We use a set to filter out duplicates - const userPermissions = new Set(); - for (const role of userRoles.roles) { - const rolePermissions = await recipe.recipeInterfaceImpl.getPermissionsForRole({ - role, - userContext, - }); - if (rolePermissions.status === "OK") { - for (const perm of rolePermissions.permissions) { - userPermissions.add(perm); - } - } - } - - return Array.from(userPermissions); - }, - defaultMaxAgeInSeconds: 300, - }); - } -} - -export const PermissionClaim = new PermissionClaimClass(); diff --git a/lib/ts/recipe/userroles/recipe.ts b/lib/ts/recipe/userroles/recipe.ts deleted file mode 100644 index 2ca5e470d..000000000 --- a/lib/ts/recipe/userroles/recipe.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import SuperTokensError from "../../error"; -import error from "../../error"; -import { BaseRequest, BaseResponse } from "../../framework"; -import normalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; -import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; - -import RecipeImplementation from "./recipeImplementation"; -import { RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; -import OverrideableBuilder from "supertokens-js-override"; -import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; -import SessionRecipe from "../session/recipe"; -import { UserRoleClaim } from "./userRoleClaim"; -import { PermissionClaim } from "./permissionClaim"; - -export default class Recipe extends RecipeModule { - static RECIPE_ID = "userroles"; - private static instance: Recipe | undefined = undefined; - - config: TypeNormalisedInput; - recipeInterfaceImpl: RecipeInterface; - isInServerlessEnv: boolean; - - constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { - super(recipeId, appInfo); - this.config = validateAndNormaliseUserInput(this, appInfo, config); - this.isInServerlessEnv = isInServerlessEnv; - - { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); - this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); - } - - PostSuperTokensInitCallbacks.addPostInitCallback(() => { - if (!this.config.skipAddingRolesToAccessToken) { - SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(UserRoleClaim); - } - if (!this.config.skipAddingPermissionsToAccessToken) { - SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(PermissionClaim); - } - }); - } - - /* Init functions */ - - static getInstanceOrThrowError(): Recipe { - if (Recipe.instance !== undefined) { - return Recipe.instance; - } - throw new Error( - "Initialisation not done. Did you forget to call the UserRoles.init or SuperTokens.init functions?" - ); - } - - static init(config?: TypeInput): RecipeListFunction { - return (appInfo, isInServerlessEnv) => { - if (Recipe.instance === undefined) { - Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); - return Recipe.instance; - } else { - throw new Error("UserRoles recipe has already been initialised. Please check your code for bugs."); - } - }; - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Recipe.instance = undefined; - } - - /* RecipeModule functions */ - - getAPIsHandled(): APIHandled[] { - return []; - } - - // This stub is required to implement RecipeModule - handleAPIRequest = async ( - _: string, - __: BaseRequest, - ___: BaseResponse, - ____: normalisedURLPath, - _____: HTTPMethod - ): Promise => { - throw new Error("Should never come here"); - }; - - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise { - throw error; - } - - getAllCORSHeaders(): string[] { - return []; - } - - isErrorFromThisRecipe(err: any): err is error { - return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; - } -} diff --git a/lib/ts/recipe/userroles/recipeImplementation.ts b/lib/ts/recipe/userroles/recipeImplementation.ts deleted file mode 100644 index 577244a17..000000000 --- a/lib/ts/recipe/userroles/recipeImplementation.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { RecipeInterface } from "./types"; -import NormalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; - -export default function getRecipeInterface(querier: Querier): RecipeInterface { - return { - addRoleToUser: function ({ userId, role }) { - return querier.sendPutRequest(new NormalisedURLPath("/recipe/user/role"), { userId, role }); - }, - - removeUserRole: function ({ userId, role }) { - return querier.sendPostRequest(new NormalisedURLPath("/recipe/user/role/remove"), { userId, role }); - }, - - getRolesForUser: function ({ userId }) { - return querier.sendGetRequest(new NormalisedURLPath("/recipe/user/roles"), { userId }); - }, - - getUsersThatHaveRole: function ({ role }) { - return querier.sendGetRequest(new NormalisedURLPath("/recipe/role/users"), { role }); - }, - - createNewRoleOrAddPermissions: function ({ role, permissions }) { - return querier.sendPutRequest(new NormalisedURLPath("/recipe/role"), { role, permissions }); - }, - - getPermissionsForRole: function ({ role }) { - return querier.sendGetRequest(new NormalisedURLPath("/recipe/role/permissions"), { role }); - }, - - removePermissionsFromRole: function ({ role, permissions }) { - return querier.sendPostRequest(new NormalisedURLPath("/recipe/role/permissions/remove"), { - role, - permissions, - }); - }, - - getRolesThatHavePermission: function ({ permission }) { - return querier.sendGetRequest(new NormalisedURLPath("/recipe/permission/roles"), { permission }); - }, - - deleteRole: function ({ role }) { - return querier.sendPostRequest(new NormalisedURLPath("/recipe/role/remove"), { role }); - }, - - getAllRoles: function () { - return querier.sendGetRequest(new NormalisedURLPath("/recipe/roles"), {}); - }, - }; -} diff --git a/lib/ts/recipe/userroles/types.ts b/lib/ts/recipe/userroles/types.ts deleted file mode 100644 index 96dfb828c..000000000 --- a/lib/ts/recipe/userroles/types.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import OverrideableBuilder from "supertokens-js-override"; - -export type TypeInput = { - skipAddingRolesToAccessToken?: boolean; - skipAddingPermissionsToAccessToken?: boolean; - override?: { - functions?: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type TypeNormalisedInput = { - skipAddingRolesToAccessToken: boolean; - skipAddingPermissionsToAccessToken: boolean; - override: { - functions: ( - originalImplementation: RecipeInterface, - builder?: OverrideableBuilder - ) => RecipeInterface; - apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; - }; -}; - -export type APIInterface = {}; - -export type RecipeInterface = { - addRoleToUser: (input: { - userId: string; - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - didUserAlreadyHaveRole: boolean; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - - removeUserRole: (input: { - userId: string; - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - didUserHaveRole: boolean; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - - getRolesForUser: (input: { - userId: string; - userContext: any; - }) => Promise<{ - status: "OK"; - roles: string[]; - }>; - - getUsersThatHaveRole: (input: { - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - users: string[]; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - - createNewRoleOrAddPermissions: (input: { - role: string; - permissions: string[]; - userContext: any; - }) => Promise<{ - status: "OK"; - createdNewRole: boolean; - }>; - - getPermissionsForRole: (input: { - role: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - permissions: string[]; - } - | { - status: "UNKNOWN_ROLE_ERROR"; - } - >; - - removePermissionsFromRole: (input: { - role: string; - permissions: string[]; - userContext: any; - }) => Promise<{ - status: "OK" | "UNKNOWN_ROLE_ERROR"; - }>; - - getRolesThatHavePermission: (input: { - permission: string; - userContext: any; - }) => Promise<{ - status: "OK"; - roles: string[]; - }>; - - deleteRole: (input: { - role: string; - userContext: any; - }) => Promise<{ - status: "OK"; - didRoleExist: boolean; - }>; - - getAllRoles: (input: { - userContext: any; - }) => Promise<{ - status: "OK"; - roles: string[]; - }>; -}; diff --git a/lib/ts/recipe/userroles/userRoleClaim.ts b/lib/ts/recipe/userroles/userRoleClaim.ts deleted file mode 100644 index ab7bb69bd..000000000 --- a/lib/ts/recipe/userroles/userRoleClaim.ts +++ /dev/null @@ -1,24 +0,0 @@ -import UserRoleRecipe from "./recipe"; -import { PrimitiveArrayClaim } from "../session/claimBaseClasses/primitiveArrayClaim"; - -/** - * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. - * */ -export class UserRoleClaimClass extends PrimitiveArrayClaim { - constructor() { - super({ - key: "st-role", - async fetchValue(userId, userContext) { - const recipe = UserRoleRecipe.getInstanceOrThrowError(); - const res = await recipe.recipeInterfaceImpl.getRolesForUser({ - userId, - userContext, - }); - return res.roles; - }, - defaultMaxAgeInSeconds: 300, - }); - } -} - -export const UserRoleClaim = new UserRoleClaimClass(); diff --git a/lib/ts/recipe/userroles/utils.ts b/lib/ts/recipe/userroles/utils.ts deleted file mode 100644 index f4602fe4c..000000000 --- a/lib/ts/recipe/userroles/utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { NormalisedAppinfo } from "../../types"; -import Recipe from "./recipe"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; - -export function validateAndNormaliseUserInput( - _: Recipe, - __: NormalisedAppinfo, - config?: TypeInput -): TypeNormalisedInput { - let override = { - functions: (originalImplementation: RecipeInterface) => originalImplementation, - apis: (originalImplementation: APIInterface) => originalImplementation, - ...config?.override, - }; - - return { - skipAddingRolesToAccessToken: config?.skipAddingRolesToAccessToken === true, - skipAddingPermissionsToAccessToken: config?.skipAddingPermissionsToAccessToken === true, - override, - }; -} diff --git a/lib/ts/recipeModule.ts b/lib/ts/recipeModule.ts deleted file mode 100644 index 4f0ae83bf..000000000 --- a/lib/ts/recipeModule.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "./error"; -import { NormalisedAppinfo, APIHandled, HTTPMethod } from "./types"; -import NormalisedURLPath from "./normalisedURLPath"; -import { BaseRequest, BaseResponse } from "./framework"; - -export default abstract class RecipeModule { - private recipeId: string; - - private appInfo: NormalisedAppinfo; - - constructor(recipeId: string, appInfo: NormalisedAppinfo) { - this.recipeId = recipeId; - this.appInfo = appInfo; - } - - getRecipeId = (): string => { - return this.recipeId; - }; - - getAppInfo = (): NormalisedAppinfo => { - return this.appInfo; - }; - - returnAPIIdIfCanHandleRequest = (path: NormalisedURLPath, method: HTTPMethod): string | undefined => { - let apisHandled = this.getAPIsHandled(); - for (let i = 0; i < apisHandled.length; i++) { - let currAPI = apisHandled[i]; - if ( - !currAPI.disabled && - currAPI.method === method && - this.appInfo.apiBasePath.appendPath(currAPI.pathWithoutApiBasePath).equals(path) - ) { - return currAPI.id; - } - } - return undefined; - }; - - abstract getAPIsHandled(): APIHandled[]; - - abstract handleAPIRequest( - id: string, - req: BaseRequest, - response: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ): Promise; - - abstract handleError(error: STError, request: BaseRequest, response: BaseResponse): Promise; - - abstract getAllCORSHeaders(): string[]; - - abstract isErrorFromThisRecipe(err: any): err is STError; -} diff --git a/lib/ts/supertokens.ts b/lib/ts/supertokens.ts deleted file mode 100644 index f83b57b46..000000000 --- a/lib/ts/supertokens.ts +++ /dev/null @@ -1,411 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import { TypeInput, NormalisedAppinfo, HTTPMethod, SuperTokensInfo } from "./types"; -import { - normaliseInputAppInfoOrThrowError, - maxVersion, - normaliseHttpMethod, - sendNon200ResponseWithMessage, - getRidFromHeader, -} from "./utils"; -import { Querier } from "./querier"; -import RecipeModule from "./recipeModule"; -import { HEADER_RID, HEADER_FDI } from "./constants"; -import NormalisedURLDomain from "./normalisedURLDomain"; -import NormalisedURLPath from "./normalisedURLPath"; -import { BaseRequest, BaseResponse } from "./framework"; -import { TypeFramework } from "./framework/types"; -import STError from "./error"; -import { logDebugMessage } from "./logger"; -import { PostSuperTokensInitCallbacks } from "./postSuperTokensInitCallbacks"; - -export default class SuperTokens { - private static instance: SuperTokens | undefined; - - framework: TypeFramework; - - appInfo: NormalisedAppinfo; - - isInServerlessEnv: boolean; - - recipeModules: RecipeModule[]; - - supertokens: undefined | SuperTokensInfo; - - telemetryEnabled: boolean; - - constructor(config: TypeInput) { - logDebugMessage("Started SuperTokens with debug logging (supertokens.init called)"); - logDebugMessage("appInfo: " + JSON.stringify(config.appInfo)); - - this.framework = config.framework !== undefined ? config.framework : "express"; - logDebugMessage("framework: " + this.framework); - this.appInfo = normaliseInputAppInfoOrThrowError(config.appInfo); - this.supertokens = config.supertokens; - - Querier.init( - config.supertokens?.connectionURI - .split(";") - .filter((h) => h !== "") - .map((h) => { - return { - domain: new NormalisedURLDomain(h.trim()), - basePath: new NormalisedURLPath(h.trim()), - }; - }), - config.supertokens?.apiKey - ); - if (config.recipeList === undefined || config.recipeList.length === 0) { - throw new Error("Please provide at least one recipe to the supertokens.init function call"); - } - - // @ts-ignore - if (config.recipeList.includes(undefined)) { - // related to issue #270. If user makes mistake by adding empty items in the recipeList, this will catch the mistake and throw relevant error - throw new Error("Please remove empty items from recipeList"); - } - - this.isInServerlessEnv = config.isInServerlessEnv === undefined ? false : config.isInServerlessEnv; - - this.recipeModules = config.recipeList.map((func) => { - return func(this.appInfo, this.isInServerlessEnv); - }); - - this.telemetryEnabled = config.telemetry === undefined ? process.env.TEST_MODE !== "testing" : config.telemetry; - } - - static init(config: TypeInput) { - if (SuperTokens.instance === undefined) { - SuperTokens.instance = new SuperTokens(config); - PostSuperTokensInitCallbacks.runPostInitCallbacks(); - } - } - - static reset() { - if (process.env.TEST_MODE !== "testing") { - throw new Error("calling testing function in non testing env"); - } - Querier.reset(); - SuperTokens.instance = undefined; - } - - static getInstanceOrThrowError(): SuperTokens { - if (SuperTokens.instance !== undefined) { - return SuperTokens.instance; - } - throw new Error("Initialisation not done. Did you forget to call the SuperTokens.init function?"); - } - - handleAPI = async ( - matchedRecipe: RecipeModule, - id: string, - request: BaseRequest, - response: BaseResponse, - path: NormalisedURLPath, - method: HTTPMethod - ) => { - return await matchedRecipe.handleAPIRequest(id, request, response, path, method); - }; - - getAllCORSHeaders = (): string[] => { - let headerSet = new Set(); - headerSet.add(HEADER_RID); - headerSet.add(HEADER_FDI); - this.recipeModules.forEach((recipe) => { - let headers = recipe.getAllCORSHeaders(); - headers.forEach((h) => { - headerSet.add(h); - }); - }); - return Array.from(headerSet); - }; - - getUserCount = async (includeRecipeIds?: string[]): Promise => { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error( - "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUserCount() instead (for example, EmailPassword.getUserCount())" - ); - } - let includeRecipeIdsStr = undefined; - if (includeRecipeIds !== undefined) { - includeRecipeIdsStr = includeRecipeIds.join(","); - } - let response = await querier.sendGetRequest(new NormalisedURLPath("/users/count"), { - includeRecipeIds: includeRecipeIdsStr, - }); - return Number(response.count); - }; - - getUsers = async (input: { - timeJoinedOrder: "ASC" | "DESC"; - limit?: number; - paginationToken?: string; - includeRecipeIds?: string[]; - query?: object; - }): Promise<{ - users: { recipeId: string; user: any }[]; - nextPaginationToken?: string; - }> => { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - throw new Error( - "Please use core version >= 3.5 to call this function. Otherwise, you can call .getUsersOldestFirst() or .getUsersNewestFirst() instead (for example, EmailPassword.getUsersOldestFirst())" - ); - } - let includeRecipeIdsStr = undefined; - if (input.includeRecipeIds !== undefined) { - includeRecipeIdsStr = input.includeRecipeIds.join(","); - } - let response = await querier.sendGetRequest(new NormalisedURLPath("/users"), { - ...input.query, - includeRecipeIds: includeRecipeIdsStr, - timeJoinedOrder: input.timeJoinedOrder, - limit: input.limit, - paginationToken: input.paginationToken, - }); - return { - users: response.users, - nextPaginationToken: response.nextPaginationToken, - }; - }; - - deleteUser = async (input: { userId: string }): Promise<{ status: "OK" }> => { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = await querier.getAPIVersion(); - if (maxVersion("2.10", cdiVersion) === cdiVersion) { - // delete user is only available >= CDI 2.10 - await querier.sendPostRequest(new NormalisedURLPath("/user/remove"), { - userId: input.userId, - }); - - return { - status: "OK", - }; - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.7.0"); - } - }; - - createUserIdMapping = async function (input: { - superTokensUserId: string; - externalUserId: string; - externalUserIdInfo?: string; - force?: boolean; - }): Promise< - | { - status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; - } - | { - status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"; - doesSuperTokensUserIdExist: boolean; - doesExternalUserIdExist: boolean; - } - > { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = await querier.getAPIVersion(); - if (maxVersion("2.15", cdiVersion) === cdiVersion) { - // create userId mapping is only available >= CDI 2.15 - return await querier.sendPostRequest(new NormalisedURLPath("/recipe/userid/map"), { - superTokensUserId: input.superTokensUserId, - externalUserId: input.externalUserId, - externalUserIdInfo: input.externalUserIdInfo, - force: input.force, - }); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }; - - getUserIdMapping = async function (input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - }): Promise< - | { - status: "OK"; - superTokensUserId: string; - externalUserId: string; - externalUserIdInfo: string | undefined; - } - | { - status: "UNKNOWN_MAPPING_ERROR"; - } - > { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = await querier.getAPIVersion(); - if (maxVersion("2.15", cdiVersion) === cdiVersion) { - // create userId mapping is only available >= CDI 2.15 - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/userid/map"), { - userId: input.userId, - userIdType: input.userIdType, - }); - return response; - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }; - - deleteUserIdMapping = async function (input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - force?: boolean; - }): Promise<{ - status: "OK"; - didMappingExist: boolean; - }> { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = await querier.getAPIVersion(); - if (maxVersion("2.15", cdiVersion) === cdiVersion) { - return await querier.sendPostRequest(new NormalisedURLPath("/recipe/userid/map/remove"), { - userId: input.userId, - userIdType: input.userIdType, - force: input.force, - }); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }; - - updateOrDeleteUserIdMappingInfo = async function (input: { - userId: string; - userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - externalUserIdInfo?: string; - }): Promise<{ - status: "OK" | "UNKNOWN_MAPPING_ERROR"; - }> { - let querier = Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = await querier.getAPIVersion(); - if (maxVersion("2.15", cdiVersion) === cdiVersion) { - return await querier.sendPutRequest(new NormalisedURLPath("/recipe/userid/external-user-id-info"), { - userId: input.userId, - userIdType: input.userIdType, - externalUserIdInfo: input.externalUserIdInfo, - }); - } else { - throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); - } - }; - - middleware = async (request: BaseRequest, response: BaseResponse): Promise => { - logDebugMessage("middleware: Started"); - let path = this.appInfo.apiGatewayPath.appendPath(new NormalisedURLPath(request.getOriginalURL())); - let method: HTTPMethod = normaliseHttpMethod(request.getMethod()); - - // if the prefix of the URL doesn't match the base path, we skip - if (!path.startsWith(this.appInfo.apiBasePath)) { - logDebugMessage( - "middleware: Not handling because request path did not start with config path. Request path: " + - path.getAsStringDangerous() - ); - return false; - } - - let requestRID = getRidFromHeader(request); - logDebugMessage("middleware: requestRID is: " + requestRID); - if (requestRID === "anti-csrf") { - // see https://github.com/supertokens/supertokens-node/issues/202 - requestRID = undefined; - } - if (requestRID !== undefined) { - let matchedRecipe: RecipeModule | undefined = undefined; - - // we loop through all recipe modules to find the one with the matching rId - for (let i = 0; i < this.recipeModules.length; i++) { - logDebugMessage("middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId()); - if (this.recipeModules[i].getRecipeId() === requestRID) { - matchedRecipe = this.recipeModules[i]; - break; - } - } - - if (matchedRecipe === undefined) { - logDebugMessage("middleware: Not handling because no recipe matched"); - // we could not find one, so we skip - return false; - } - logDebugMessage("middleware: Matched with recipe ID: " + matchedRecipe.getRecipeId()); - - let id = matchedRecipe.returnAPIIdIfCanHandleRequest(path, method); - if (id === undefined) { - logDebugMessage( - "middleware: Not handling because recipe doesn't handle request path or method. Request path: " + - path.getAsStringDangerous() + - ", request method: " + - method - ); - // the matched recipe doesn't handle this path and http method - return false; - } - - logDebugMessage("middleware: Request being handled by recipe. ID is: " + id); - - // give task to the matched recipe - let requestHandled = await matchedRecipe.handleAPIRequest(id, request, response, path, method); - if (!requestHandled) { - logDebugMessage("middleware: Not handled because API returned requestHandled as false"); - return false; - } - logDebugMessage("middleware: Ended"); - return true; - } else { - // we loop through all recipe modules to find the one with the matching path and method - for (let i = 0; i < this.recipeModules.length; i++) { - logDebugMessage("middleware: Checking recipe ID for match: " + this.recipeModules[i].getRecipeId()); - let id = this.recipeModules[i].returnAPIIdIfCanHandleRequest(path, method); - if (id !== undefined) { - logDebugMessage("middleware: Request being handled by recipe. ID is: " + id); - let requestHandled = await this.recipeModules[i].handleAPIRequest( - id, - request, - response, - path, - method - ); - if (!requestHandled) { - logDebugMessage("middleware: Not handled because API returned requestHandled as false"); - return false; - } - logDebugMessage("middleware: Ended"); - return true; - } - } - logDebugMessage("middleware: Not handling because no recipe matched"); - return false; - } - }; - - errorHandler = async (err: any, request: BaseRequest, response: BaseResponse) => { - logDebugMessage("errorHandler: Started"); - if (STError.isErrorFromSuperTokens(err)) { - logDebugMessage("errorHandler: Error is from SuperTokens recipe. Message: " + err.message); - if (err.type === STError.BAD_INPUT_ERROR) { - logDebugMessage("errorHandler: Sending 400 status code response"); - return sendNon200ResponseWithMessage(response, err.message, 400); - } - - for (let i = 0; i < this.recipeModules.length; i++) { - logDebugMessage("errorHandler: Checking recipe for match: " + this.recipeModules[i].getRecipeId()); - if (this.recipeModules[i].isErrorFromThisRecipe(err)) { - logDebugMessage("errorHandler: Matched with recipeID: " + this.recipeModules[i].getRecipeId()); - return await this.recipeModules[i].handleError(err, request, response); - } - } - } - throw err; - }; -} diff --git a/lib/ts/types.ts b/lib/ts/types.ts deleted file mode 100644 index 22a278ac3..000000000 --- a/lib/ts/types.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import RecipeModule from "./recipeModule"; -import NormalisedURLDomain from "./normalisedURLDomain"; -import NormalisedURLPath from "./normalisedURLPath"; -import { TypeFramework } from "./framework/types"; - -export type AppInfo = { - appName: string; - websiteDomain: string; - websiteBasePath?: string; - apiDomain: string; - apiBasePath?: string; - apiGatewayPath?: string; -}; - -export type NormalisedAppinfo = { - appName: string; - websiteDomain: NormalisedURLDomain; - apiDomain: NormalisedURLDomain; - topLevelAPIDomain: string; - topLevelWebsiteDomain: string; - apiBasePath: NormalisedURLPath; - apiGatewayPath: NormalisedURLPath; - websiteBasePath: NormalisedURLPath; -}; - -export type SuperTokensInfo = { - connectionURI: string; - apiKey?: string; -}; - -export type TypeInput = { - supertokens?: SuperTokensInfo; - framework?: TypeFramework; - appInfo: AppInfo; - recipeList: RecipeListFunction[]; - telemetry?: boolean; - isInServerlessEnv?: boolean; -}; - -export type RecipeListFunction = (appInfo: NormalisedAppinfo, isInServerlessEnv: boolean) => RecipeModule; - -export type APIHandled = { - pathWithoutApiBasePath: NormalisedURLPath; - method: HTTPMethod; - id: string; - disabled: boolean; -}; - -export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; - -export type JSONPrimitive = string | number | boolean | null; -export type JSONArray = Array; -export type JSONValue = JSONPrimitive | JSONObject | JSONArray | undefined; -export interface JSONObject { - [ind: string]: JSONValue; -} -export type GeneralErrorResponse = { - status: "GENERAL_ERROR"; - message: string; -}; diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts deleted file mode 100644 index ce1bcab53..000000000 --- a/lib/ts/utils.ts +++ /dev/null @@ -1,168 +0,0 @@ -import * as psl from "psl"; - -import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject } from "./types"; -import NormalisedURLDomain from "./normalisedURLDomain"; -import NormalisedURLPath from "./normalisedURLPath"; -import type { BaseRequest, BaseResponse } from "./framework"; -import { logDebugMessage } from "./logger"; -import { HEADER_RID } from "./constants"; - -export function getLargestVersionFromIntersection(v1: string[], v2: string[]): string | undefined { - let intersection = v1.filter((value) => v2.indexOf(value) !== -1); - if (intersection.length === 0) { - return undefined; - } - let maxVersionSoFar = intersection[0]; - for (let i = 1; i < intersection.length; i++) { - maxVersionSoFar = maxVersion(intersection[i], maxVersionSoFar); - } - return maxVersionSoFar; -} - -export function maxVersion(version1: string, version2: string): string { - let splittedv1 = version1.split("."); - let splittedv2 = version2.split("."); - let minLength = Math.min(splittedv1.length, splittedv2.length); - for (let i = 0; i < minLength; i++) { - let v1 = Number(splittedv1[i]); - let v2 = Number(splittedv2[i]); - if (v1 > v2) { - return version1; - } else if (v2 > v1) { - return version2; - } - } - if (splittedv1.length >= splittedv2.length) { - return version1; - } - return version2; -} - -export function normaliseInputAppInfoOrThrowError(appInfo: AppInfo): NormalisedAppinfo { - if (appInfo === undefined) { - throw new Error("Please provide the appInfo object when calling supertokens.init"); - } - if (appInfo.apiDomain === undefined) { - throw new Error("Please provide your apiDomain inside the appInfo object when calling supertokens.init"); - } - if (appInfo.appName === undefined) { - throw new Error("Please provide your appName inside the appInfo object when calling supertokens.init"); - } - if (appInfo.websiteDomain === undefined) { - throw new Error("Please provide your websiteDomain inside the appInfo object when calling supertokens.init"); - } - let apiGatewayPath = - appInfo.apiGatewayPath !== undefined - ? new NormalisedURLPath(appInfo.apiGatewayPath) - : new NormalisedURLPath(""); - - const websiteDomain = new NormalisedURLDomain(appInfo.websiteDomain); - const apiDomain = new NormalisedURLDomain(appInfo.apiDomain); - const topLevelAPIDomain = getTopLevelDomainForSameSiteResolution(apiDomain.getAsStringDangerous()); - const topLevelWebsiteDomain = getTopLevelDomainForSameSiteResolution(websiteDomain.getAsStringDangerous()); - - return { - appName: appInfo.appName, - websiteDomain, - apiDomain, - apiBasePath: apiGatewayPath.appendPath( - appInfo.apiBasePath === undefined - ? new NormalisedURLPath("/auth") - : new NormalisedURLPath(appInfo.apiBasePath) - ), - websiteBasePath: - appInfo.websiteBasePath === undefined - ? new NormalisedURLPath("/auth") - : new NormalisedURLPath(appInfo.websiteBasePath), - apiGatewayPath, - topLevelAPIDomain, - topLevelWebsiteDomain, - }; -} - -export function normaliseHttpMethod(method: string): HTTPMethod { - return method.toLowerCase() as HTTPMethod; -} - -export function sendNon200ResponseWithMessage(res: BaseResponse, message: string, statusCode: number) { - sendNon200Response(res, statusCode, { message }); -} - -export function sendNon200Response(res: BaseResponse, statusCode: number, body: JSONObject) { - if (statusCode < 300) { - throw new Error("Calling sendNon200Response with status code < 300"); - } - logDebugMessage("Sending response to client with status code: " + statusCode); - res.setStatusCode(statusCode); - res.sendJSONResponse(body); -} - -export function send200Response(res: BaseResponse, responseJson: any) { - logDebugMessage("Sending response to client with status code: 200"); - res.setStatusCode(200); - res.sendJSONResponse(responseJson); -} - -export function isAnIpAddress(ipaddress: string) { - return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( - ipaddress - ); -} - -export function getRidFromHeader(req: BaseRequest): string | undefined { - return req.getHeaderValue(HEADER_RID); -} - -export function frontendHasInterceptor(req: BaseRequest): boolean { - return getRidFromHeader(req) !== undefined; -} - -export function humaniseMilliseconds(ms: number): string { - let t = Math.floor(ms / 1000); - let suffix = ""; - - if (t < 60) { - if (t > 1) suffix = "s"; - return `${t} second${suffix}`; - } else if (t < 3600) { - const m = Math.floor(t / 60); - if (m > 1) suffix = "s"; - return `${m} minute${suffix}`; - } else { - const h = Math.floor(t / 360) / 10; - if (h > 1) suffix = "s"; - return `${h} hour${suffix}`; - } -} - -export function makeDefaultUserContextFromAPI(request: BaseRequest): any { - return { - _default: { - request, - }, - }; -} - -export function getTopLevelDomainForSameSiteResolution(url: string): string { - let urlObj = new URL(url); - let hostname = urlObj.hostname; - if (hostname.startsWith("localhost") || hostname.startsWith("localhost.org") || isAnIpAddress(hostname)) { - // we treat these as the same TLDs since we can use sameSite lax for all of them. - return "localhost"; - } - let parsedURL = psl.parse(hostname) as psl.ParsedDomain; - if (parsedURL.domain === null) { - throw new Error("Please make sure that the apiDomain and websiteDomain have correct values"); - } - return parsedURL.domain; -} - -export function getFromObjectCaseInsensitive(key: string, object: Record): T | undefined { - const matchedKeys = Object.keys(object).filter((i) => i.toLocaleLowerCase() === key.toLocaleLowerCase()); - - if (matchedKeys.length === 0) { - return undefined; - } - - return object[matchedKeys[0]]; -} diff --git a/lib/tsconfig.json b/lib/tsconfig.json deleted file mode 100644 index 537bb3a43..000000000 --- a/lib/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2016", - "strictNullChecks": true, - "declaration": true, - "module": "commonJS", - "outDir": "build", - "moduleResolution": "Node", - "noImplicitAny": true, - "sourceMap": false, - "noImplicitThis": false, - "skipLibCheck": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "lib": ["ES2017"], - "esModuleInterop": true - }, - "include": ["ts/**/*"], - "exclude": ["build"], - "compileOnSave": true -} diff --git a/lib/tslint.json b/lib/tslint.json deleted file mode 100644 index 992cc5378..000000000 --- a/lib/tslint.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "jsRules": { - "class-name": true, - "comment-format": [true, "check-space"], - "indent": [true, "spaces"], - "no-duplicate-variable": true, - "no-eval": true, - "no-trailing-whitespace": true, - "no-unsafe-finally": true, - "one-line": [true, "check-open-brace", "check-whitespace"], - "quotemark": [true, "double"], - "semicolon": [true, "always"], - "triple-equals": [true, "allow-null-check"], - "variable-name": [true, "ban-keywords"], - "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"] - }, - "rules": { - "class-name": true, - "comment-format": [true, "check-space"], - "indent": [true, "spaces"], - "no-eval": true, - "no-internal-module": true, - "no-trailing-whitespace": true, - "no-unsafe-finally": true, - "no-var-keyword": true, - "one-line": [true, "check-open-brace", "check-whitespace"], - "quotemark": [true, "double"], - "semicolon": [true, "always"], - "triple-equals": [true, "allow-null-check"], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "variable-name": [true, "ban-keywords"], - "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"] - } -} diff --git a/nextjs/index.d.ts b/nextjs/index.d.ts deleted file mode 100644 index 8a9bc0b8f..000000000 --- a/nextjs/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "../lib/build/nextjs"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ - -import * as _default from "../lib/build/nextjs"; -export default _default; diff --git a/nextjs/index.js b/nextjs/index.js deleted file mode 100644 index d95450f5c..000000000 --- a/nextjs/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../lib/build/nextjs")); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fb508ac88..000000000 --- a/package-lock.json +++ /dev/null @@ -1,17486 +0,0 @@ -{ - "name": "supertokens-node", - "version": "13.6.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "supertokens-node", - "version": "13.6.0", - "license": "Apache-2.0", - "dependencies": { - "axios": "0.21.4", - "body-parser": "1.20.1", - "co-body": "6.1.0", - "cookie": "0.4.0", - "debug": "^4.3.3", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^2.0.5", - "libphonenumber-js": "^1.9.44", - "nodemailer": "^6.7.2", - "psl": "1.8.0", - "supertokens-js-override": "^0.0.4", - "twilio": "^4.7.2", - "verify-apple-id-token": "^3.0.1" - }, - "devDependencies": { - "@hapi/hapi": "^20.2.0", - "@koa/router": "^10.1.1", - "@loopback/core": "2.16.2", - "@loopback/repository": "3.7.1", - "@loopback/rest": "9.3.0", - "@types/aws-lambda": "8.10.77", - "@types/co-body": "^5.1.1", - "@types/cookie": "0.3.3", - "@types/express": "4.16.1", - "@types/hapi__hapi": "20.0.8", - "@types/jsonwebtoken": "9.0.0", - "@types/koa": "^2.13.4", - "@types/koa-bodyparser": "^4.3.3", - "@types/nodemailer": "^6.4.4", - "@types/psl": "1.1.0", - "@types/validator": "10.11.0", - "aws-sdk-mock": "^5.4.0", - "cookie-parser": "^1.4.5", - "express": "^4.18.2", - "fastify": "3.18.1", - "glob": "7.1.7", - "koa": "^2.13.3", - "lambda-tester": "^4.0.1", - "loopback-datasource-juggler": "^4.26.0", - "mocha": "6.1.4", - "next": "11.1.3", - "next-test-api-route-handler": "^3.1.8", - "nock": "11.7.0", - "prettier": "2.0.5", - "pretty-quick": "^3.1.1", - "react": "^17.0.2", - "sinon": "^14.0.0", - "supertest": "4.0.2", - "typedoc": "^0.22.5", - "typescript": "4.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", - "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.17.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.5.tgz", - "integrity": "sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA==", - "dev": true, - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.17.2", - "@babel/parser": "^7.17.3", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", - "integrity": "sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/browserslist": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.3.tgz", - "integrity": "sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==", - "dev": true, - "peer": true, - "dependencies": { - "caniuse-lite": "^1.0.30001312", - "electron-to-chromium": "^1.4.71", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", - "dev": true, - "peer": true - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-get-function-arity/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.2.tgz", - "integrity": "sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.3.tgz", - "integrity": "sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==", - "dev": true, - "peer": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", - "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz", - "integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", - "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.14.9", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@extra-number/significant-digits": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@extra-number/significant-digits/-/significant-digits-1.3.9.tgz", - "integrity": "sha512-E5PY/bCwrNqEHh4QS6AQBinLZ+sxM1lT8tsSVYk8VwhWIPp6fCU/BMRVq0V8iJ8LwS3FHmaA4vUzb78s4BIIyA==", - "dev": true - }, - "node_modules/@fastify/ajv-compiler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", - "integrity": "sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg==", - "dev": true, - "dependencies": { - "ajv": "^6.12.6" - } - }, - "node_modules/@hapi/accept": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-5.0.2.tgz", - "integrity": "sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/ammo": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-5.0.1.tgz", - "integrity": "sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==", - "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/b64": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-5.0.0.tgz", - "integrity": "sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/bounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-2.0.0.tgz", - "integrity": "sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/bourne": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.0.0.tgz", - "integrity": "sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==", - "dev": true - }, - "node_modules/@hapi/call": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@hapi/call/-/call-8.0.1.tgz", - "integrity": "sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/catbox": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-11.1.1.tgz", - "integrity": "sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/podium": "4.x.x", - "@hapi/validate": "1.x.x" - } - }, - "node_modules/@hapi/catbox-memory": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz", - "integrity": "sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/content": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@hapi/content/-/content-5.0.2.tgz", - "integrity": "sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x" - } - }, - "node_modules/@hapi/cryptiles": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-5.1.0.tgz", - "integrity": "sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@hapi/file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/file/-/file-2.0.0.tgz", - "integrity": "sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==", - "dev": true - }, - "node_modules/@hapi/hapi": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-20.2.1.tgz", - "integrity": "sha512-OXAU+yWLwkMfPFic+KITo+XPp6Oxpgc9WUH+pxXWcTIuvWbgco5TC/jS8UDvz+NFF5IzRgF2CL6UV/KLdQYUSQ==", - "dev": true, - "dependencies": { - "@hapi/accept": "^5.0.1", - "@hapi/ammo": "^5.0.1", - "@hapi/boom": "^9.1.0", - "@hapi/bounce": "^2.0.0", - "@hapi/call": "^8.0.0", - "@hapi/catbox": "^11.1.1", - "@hapi/catbox-memory": "^5.0.0", - "@hapi/heavy": "^7.0.1", - "@hapi/hoek": "^9.0.4", - "@hapi/mimos": "^6.0.0", - "@hapi/podium": "^4.1.1", - "@hapi/shot": "^5.0.5", - "@hapi/somever": "^3.0.0", - "@hapi/statehood": "^7.0.3", - "@hapi/subtext": "^7.0.3", - "@hapi/teamwork": "^5.1.0", - "@hapi/topo": "^5.0.0", - "@hapi/validate": "^1.1.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@hapi/heavy": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-7.0.1.tgz", - "integrity": "sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/validate": "1.x.x" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", - "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==", - "dev": true - }, - "node_modules/@hapi/iron": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-6.0.0.tgz", - "integrity": "sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==", - "dev": true, - "dependencies": { - "@hapi/b64": "5.x.x", - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/cryptiles": "5.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/mimos": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-6.0.0.tgz", - "integrity": "sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==", - "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x", - "mime-db": "1.x.x" - } - }, - "node_modules/@hapi/nigel": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-4.0.2.tgz", - "integrity": "sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.4", - "@hapi/vise": "^4.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@hapi/pez": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-5.0.3.tgz", - "integrity": "sha512-mpikYRJjtrbJgdDHG/H9ySqYqwJ+QU/D7FXsYciS9P7NYBXE2ayKDAy3H0ou6CohOCaxPuTV4SZ0D936+VomHA==", - "dev": true, - "dependencies": { - "@hapi/b64": "5.x.x", - "@hapi/boom": "9.x.x", - "@hapi/content": "^5.0.2", - "@hapi/hoek": "9.x.x", - "@hapi/nigel": "4.x.x" - } - }, - "node_modules/@hapi/podium": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-4.1.3.tgz", - "integrity": "sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==", - "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x", - "@hapi/teamwork": "5.x.x", - "@hapi/validate": "1.x.x" - } - }, - "node_modules/@hapi/shot": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-5.0.5.tgz", - "integrity": "sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==", - "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x", - "@hapi/validate": "1.x.x" - } - }, - "node_modules/@hapi/somever": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-3.0.1.tgz", - "integrity": "sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w==", - "dev": true, - "dependencies": { - "@hapi/bounce": "2.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/statehood": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-7.0.3.tgz", - "integrity": "sha512-pYB+pyCHkf2Amh67QAXz7e/DN9jcMplIL7Z6N8h0K+ZTy0b404JKPEYkbWHSnDtxLjJB/OtgElxocr2fMH4G7w==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/bounce": "2.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/cryptiles": "5.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/iron": "6.x.x", - "@hapi/validate": "1.x.x" - } - }, - "node_modules/@hapi/subtext": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-7.0.3.tgz", - "integrity": "sha512-CekDizZkDGERJ01C0+TzHlKtqdXZxzSWTOaH6THBrbOHnsr3GY+yiMZC+AfNCypfE17RaIakGIAbpL2Tk1z2+A==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/content": "^5.0.2", - "@hapi/file": "2.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/pez": "^5.0.1", - "@hapi/wreck": "17.x.x" - } - }, - "node_modules/@hapi/teamwork": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-5.1.0.tgz", - "integrity": "sha512-llqoQTrAJDTXxG3c4Kz/uzhBS1TsmSBa/XG5SPcVXgmffHE1nFtyLIK0hNJHCB3EuBKT84adzd1hZNY9GJLWtg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@hapi/validate": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-1.1.3.tgz", - "integrity": "sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0" - } - }, - "node_modules/@hapi/vise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-4.0.0.tgz", - "integrity": "sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==", - "dev": true, - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/wreck": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-17.1.0.tgz", - "integrity": "sha512-nx6sFyfqOpJ+EFrHX+XWwJAxs3ju4iHdbB/bwR8yTNZOiYmuhA8eCe7lYPtYmb4j7vyK/SlbaQsmTtUrMvPEBw==", - "dev": true, - "dependencies": { - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", - "dev": true, - "peer": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@koa/router": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-10.1.1.tgz", - "integrity": "sha512-ORNjq5z4EmQPriKbR0ER3k4Gh7YGNhWDL7JBW+8wXDrHLbWYKYSJaOJ9aN06npF5tbTxe2JBOsurpJDAvjiXKw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.1.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@loopback/context": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/@loopback/context/-/context-3.18.0.tgz", - "integrity": "sha512-PKx0rTguqBj6mUHBbEHLF031MnP6KiSkMLE4E8Hpy2KPJxG97HUT2ZUACHCP6qm8yS9spWQQ6g72VYAWxDrN+g==", - "dev": true, - "dependencies": { - "@loopback/metadata": "^3.3.4", - "@types/debug": "^4.1.7", - "debug": "^4.3.2", - "hyperid": "^2.3.1", - "p-event": "^4.2.0", - "tslib": "^2.3.1", - "uuid": "^8.3.2" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - } - }, - "node_modules/@loopback/core": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@loopback/core/-/core-2.16.2.tgz", - "integrity": "sha512-KtkNv6HIh8TFBOxTkfPp/BQbVqjDsGef/DtbNHH1ZHs3gSbofhkZs3IqQdYQzpkUq71mQjz5RJ/yUYY5Sqva9w==", - "dev": true, - "dependencies": { - "@loopback/context": "^3.17.1", - "debug": "^4.3.1", - "tslib": "^2.3.0" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - } - }, - "node_modules/@loopback/filter": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@loopback/filter/-/filter-1.5.4.tgz", - "integrity": "sha512-kfdCgSy0YoAFNYXJOpag5uJnlErYcROIeJqeAglkwOa3hSw2BYIudurU8hoqsiOBIGhI5BF4A3S8u4q089xWlg==", - "dev": true, - "dependencies": { - "tslib": "^2.3.1" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - } - }, - "node_modules/@loopback/http-server": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@loopback/http-server/-/http-server-2.5.4.tgz", - "integrity": "sha512-M7w+4AEhwDn7q00soCe8yYQDUS+n87ppuXQ1rJ9a1b9TdnEh+7nPFVrVpwiEKBGyVGIJWDq5BMSZYo1zMIPFUA==", - "dev": true, - "dependencies": { - "debug": "^4.3.2", - "stoppable": "^1.1.0", - "tslib": "^2.3.1" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - } - }, - "node_modules/@loopback/metadata": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-3.3.4.tgz", - "integrity": "sha512-FISs8OVYKB+wmL0VZdsDZzMOc/KC6anOf3ORpFRO2Mgl9dKCOD8IELKc8r/nr2kyD4r7/pjr5GfLy4nirS1vnQ==", - "dev": true, - "dependencies": { - "debug": "^4.3.2", - "lodash": "^4.17.21", - "reflect-metadata": "^0.1.13", - "tslib": "^2.3.1" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - } - }, - "node_modules/@loopback/repository": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-3.7.1.tgz", - "integrity": "sha512-q9vpgQ5MSZqI/ww2TTuDy0Y34NZaapS0+4ZKcwVgwH0XJFxgGwznc5W0l1Esu1lplijejpza4ItKwnlGmvYcJg==", - "dev": true, - "dependencies": { - "@loopback/filter": "^1.5.2", - "@types/debug": "^4.1.5", - "debug": "^4.3.1", - "lodash": "^4.17.21", - "loopback-datasource-juggler": "^4.26.0", - "tslib": "^2.3.0" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - }, - "peerDependencies": { - "@loopback/core": "^2.16.2" - } - }, - "node_modules/@loopback/rest": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-9.3.0.tgz", - "integrity": "sha512-Qwn5WXctQ2AV6Duze/x7ks9Tb/Oy6FSs0Hhyuhzz7aassKbu1m/GiJLB7bvpJVUN+qVZ2+vQZZ6Y3F+znzH4og==", - "dev": true, - "dependencies": { - "@loopback/express": "^3.3.0", - "@loopback/http-server": "^2.5.0", - "@loopback/openapi-v3": "^5.3.0", - "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.0", - "@types/body-parser": "^1.19.0", - "@types/cors": "^2.8.10", - "@types/express": "^4.17.11", - "@types/express-serve-static-core": "^4.17.19", - "@types/http-errors": "^1.8.0", - "@types/on-finished": "^2.3.1", - "@types/serve-static": "1.13.9", - "@types/type-is": "^1.6.3", - "ajv": "^6.12.6", - "ajv-errors": "^1.0.1", - "ajv-keywords": "^3.5.2", - "body-parser": "^1.19.0", - "cors": "^2.8.5", - "debug": "^4.3.1", - "express": "^4.17.1", - "http-errors": "^1.8.0", - "js-yaml": "^4.1.0", - "json-schema-compare": "^0.2.2", - "lodash": "^4.17.21", - "on-finished": "^2.3.0", - "path-to-regexp": "^6.2.0", - "qs": "^6.10.1", - "strong-error-handler": "^4.0.0", - "tslib": "^2.2.0", - "type-is": "^1.6.18", - "validator": "^13.6.0" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - }, - "peerDependencies": { - "@loopback/core": "^2.16.0" - } - }, - "node_modules/@loopback/rest/node_modules/@loopback/express": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@loopback/express/-/express-3.3.4.tgz", - "integrity": "sha512-y+7fu/aXGp7+5QhEnKhwmI/vKLAQtsyvEXTiT8stbj4VHWvNbUbYVwfud5IOeH7d+lhxlNQ5aEJ7IDwoM8xD6Q==", - "dev": true, - "dependencies": { - "@loopback/http-server": "^2.5.4", - "@types/body-parser": "^1.19.1", - "@types/express": "^4.17.13", - "@types/express-serve-static-core": "^4.17.24", - "@types/http-errors": "^1.8.1", - "body-parser": "^1.19.0", - "debug": "^4.3.2", - "express": "^4.17.1", - "http-errors": "^1.8.0", - "on-finished": "^2.3.0", - "toposort": "^2.0.2", - "tslib": "^2.3.1" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - }, - "peerDependencies": { - "@loopback/core": "^2.18.0" - } - }, - "node_modules/@loopback/rest/node_modules/@loopback/openapi-v3": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@loopback/openapi-v3/-/openapi-v3-5.3.1.tgz", - "integrity": "sha512-MBVamgxDDbgQQlQjIxSOnaVXLx6plxzn3e8CW8YbNc3TNiS1P8EFa5vNBp8wIzSDTeEd3ic6qzUxCUZIICiFNA==", - "dev": true, - "dependencies": { - "@loopback/repository-json-schema": "^3.4.1", - "debug": "^4.3.1", - "http-status": "^1.5.0", - "json-merge-patch": "^1.0.1", - "lodash": "^4.17.21", - "openapi3-ts": "^2.0.1", - "tslib": "^2.2.0" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - }, - "peerDependencies": { - "@loopback/core": "^2.16.1" - } - }, - "node_modules/@loopback/rest/node_modules/@loopback/openapi-v3/node_modules/@loopback/repository-json-schema": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@loopback/repository-json-schema/-/repository-json-schema-3.4.1.tgz", - "integrity": "sha512-E9UKegav+8Bp0MLPQu33c7tWUmWbnKARy0Uu2m7nvP3e3t3WOwB8U9hMjX/wBOhJ4UFJCXAXlq1MulQ/R3dyTw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.7", - "debug": "^4.3.1", - "tslib": "^2.2.0" - }, - "engines": { - "node": "^10.16 || 12 || 14 || 16" - }, - "peerDependencies": { - "@loopback/core": "^2.16.1", - "@loopback/repository": "^3.7.0" - } - }, - "node_modules/@loopback/rest/node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@napi-rs/triples": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.1.0.tgz", - "integrity": "sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w==", - "dev": true - }, - "node_modules/@next/env": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-11.1.3.tgz", - "integrity": "sha512-5+vaeooJuWmICSlmVaAC8KG3O8hwKasACVfkHj58xQuCB5SW0TKW3hWxgxkBuefMBn1nM0yEVPKokXCsYjBtng==", - "dev": true - }, - "node_modules/@next/polyfill-module": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/polyfill-module/-/polyfill-module-11.1.3.tgz", - "integrity": "sha512-7yr9cr4a0SrBoVE8psxXWK1wTFc8UzsY8Wc2cWGL7qA0hgtqACHaXC47M1ByJB410hFZenGrpE+KFaT1unQMyw==", - "dev": true - }, - "node_modules/@next/react-dev-overlay": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/react-dev-overlay/-/react-dev-overlay-11.1.3.tgz", - "integrity": "sha512-zIwtMliSUR+IKl917ToFNB+0fD7bI5kYMdjHU/UEKpfIXAZPnXRHHISCvPDsczlr+bRsbjlUFW1CsNiuFedeuQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "7.12.11", - "anser": "1.4.9", - "chalk": "4.0.0", - "classnames": "2.2.6", - "css.escape": "1.5.1", - "data-uri-to-buffer": "3.0.1", - "platform": "1.3.6", - "shell-quote": "1.7.2", - "source-map": "0.8.0-beta.0", - "stacktrace-parser": "0.1.10", - "strip-ansi": "6.0.0" - }, - "peerDependencies": { - "react": "^17.0.2", - "react-dom": "^17.0.2" - } - }, - "node_modules/@next/react-dev-overlay/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@next/react-dev-overlay/node_modules/chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@next/react-dev-overlay/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@next/react-dev-overlay/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@next/react-dev-overlay/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@next/react-dev-overlay/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@next/react-refresh-utils": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/react-refresh-utils/-/react-refresh-utils-11.1.3.tgz", - "integrity": "sha512-144kD8q2nChw67V3AJJlPQ6NUJVFczyn10bhTynn9o2rY5DEnkzuBipcyMuQl2DqfxMkV7sn+yOCOYbrLCk9zg==", - "dev": true, - "peerDependencies": { - "react-refresh": "0.8.3", - "webpack": "^4 || ^5" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.3.tgz", - "integrity": "sha512-TwP4krjhs+uU9pesDYCShEXZrLSbJr78p12e7XnLBBaNf20SgWLlVmQUT9gX9KbWan5V0sUbJfmcS8MRNHgYuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.3.tgz", - "integrity": "sha512-ZSWmkg/PxccHFNUSeBdrfaH8KwSkoeUtewXKvuYYt7Ph0yRsbqSyNIvhUezDua96lApiXXq6EL2d1THfeWomvw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.3.tgz", - "integrity": "sha512-PrTBN0iZudAuj4jSbtXcdBdmfpaDCPIneG4Oms4zcs93KwMgLhivYW082Mvlgx9QVEiRm7+RkFpIVtG/i7JitA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.3.tgz", - "integrity": "sha512-mRwbscVjRoHk+tDY7XbkT5d9FCwujFIQJpGp0XNb1i5OHCSDO8WW/C9cLEWS4LxKRbIZlTLYg1MTXqLQkvva8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/helper": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.2.1.tgz", - "integrity": "sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg==", - "dev": true, - "dependencies": { - "@napi-rs/triples": "^1.0.3" - } - }, - "node_modules/@openapi-contrib/openapi-schema-to-json-schema": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz", - "integrity": "sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/@panva/asn1.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", - "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@sideway/address": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", - "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", - "dev": true - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, - "node_modules/@types/accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/aws-lambda": { - "version": "8.10.77", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.77.tgz", - "integrity": "sha512-n0EMFJU/7u3KvHrR83l/zrKOVURXl5pUJPNED/Bzjah89QKCHwCiKCBoVUXRwTGRfCYGIDdinJaAlKDHZdp/Ng==", - "dev": true - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/co-body": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-5.1.1.tgz", - "integrity": "sha512-0/6AjTfQc5OJUchOS4OHiXNPZVuk+5XvEC2vdcizw/bwx0yb0xY7TKSf8JYvQYZ/OJDiAEjWzxnMjGPnSVlPmA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==", - "dev": true - }, - "node_modules/@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==", - "dev": true - }, - "node_modules/@types/cookies": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", - "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/express": "*", - "@types/keygrip": "*", - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true - }, - "node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", - "dev": true, - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/express": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", - "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-jwt": { - "version": "0.0.42", - "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", - "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", - "dependencies": { - "@types/express": "*", - "@types/express-unless": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/express-unless": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz", - "integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/hapi__catbox": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/@types/hapi__catbox/-/hapi__catbox-10.2.4.tgz", - "integrity": "sha512-A6ivRrXD5glmnJna1UAGw87QNZRp/vdFO9U4GS+WhOMWzHnw+oTGkMvg0g6y1930CbeheGOCm7A1qHsqH7AXqg==", - "dev": true - }, - "node_modules/@types/hapi__hapi": { - "version": "20.0.8", - "resolved": "https://registry.npmjs.org/@types/hapi__hapi/-/hapi__hapi-20.0.8.tgz", - "integrity": "sha512-NNslrYq2XQwm4uOqNcSWKpYtaeMr4DkQdrFzSB7p9rKB9ppJLh3mgP2wak9vBZl7/Cnhhb+JVBcUZCOUcW0JPA==", - "dev": true, - "dependencies": { - "@hapi/boom": "^9.0.0", - "@hapi/iron": "^6.0.0", - "@hapi/podium": "^4.1.3", - "@types/hapi__catbox": "*", - "@types/hapi__mimos": "*", - "@types/hapi__shot": "*", - "@types/node": "*", - "joi": "^17.3.0" - } - }, - "node_modules/@types/hapi__mimos": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/hapi__mimos/-/hapi__mimos-4.1.4.tgz", - "integrity": "sha512-i9hvJpFYTT/qzB5xKWvDYaSXrIiNqi4ephi+5Lo6+DoQdwqPXQgmVVOZR+s3MBiHoFqsCZCX9TmVWG3HczmTEQ==", - "dev": true, - "dependencies": { - "@types/mime-db": "*" - } - }, - "node_modules/@types/hapi__shot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/hapi__shot/-/hapi__shot-4.1.2.tgz", - "integrity": "sha512-8wWgLVP1TeGqgzZtCdt+F+k15DWQvLG1Yv6ZzPfb3D5WIo5/S+GGKtJBVo2uNEcqabP5Ifc71QnJTDnTmw1axA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-assert": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", - "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==", - "dev": true - }, - "node_modules/@types/http-errors": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", - "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-mM4TkDpA9oixqg1Fv2vVpOFyIVLJjm5x4k0V+K/rEsizfjD7Tk7LKk3GTtbB7KCfP0FEHQtsZqFxYA0+sijNVg==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/keygrip": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", - "dev": true - }, - "node_modules/@types/koa": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.4.tgz", - "integrity": "sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==", - "dev": true, - "dependencies": { - "@types/accepts": "*", - "@types/content-disposition": "*", - "@types/cookies": "*", - "@types/http-assert": "*", - "@types/http-errors": "*", - "@types/keygrip": "*", - "@types/koa-compose": "*", - "@types/node": "*" - } - }, - "node_modules/@types/koa-bodyparser": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.3.5.tgz", - "integrity": "sha512-NRqqoTtt7cfdDk/KNo+EwCIKRuzPAu/wsaZ7tgIvSIBtNfxuZHYueaLoWdxX3ZftWavQv07NE46TcpyoZGqpgQ==", - "dev": true, - "dependencies": { - "@types/koa": "*" - } - }, - "node_modules/@types/koa-compose": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", - "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", - "dev": true, - "dependencies": { - "@types/koa": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" - }, - "node_modules/@types/mime-db": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.1.tgz", - "integrity": "sha512-kGZJY+R+WnR5Rk+RPHUMERtb2qBRViIHCBdtUrY+NmwuGb8pQdfTqQiCKPrxpdoycl8KWm2DLdkpoSdt479XoQ==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, - "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "17.0.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", - "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==" - }, - "node_modules/@types/nodemailer": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", - "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/on-finished": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/on-finished/-/on-finished-2.3.1.tgz", - "integrity": "sha512-mzVYaYcFs5Jd2n/O6uYIRUsFRR1cHyZLRvkLCU0E7+G5WhY0qBDAR5fUCeZbvecYOSh9ikhlesyi2UfI8B9ckQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/psl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz", - "integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==", - "dev": true - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "node_modules/@types/serve-static": { - "version": "1.13.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", - "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/type-is": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", - "integrity": "sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-i1aY7RKb6HmQIEnK0cBmUZUp1URx0riIHw/GYNoZ46Su0GWfLiDmMI8zMRmaauMnOTg2bQag0qfwcyUFC9Tn+A==", - "dev": true - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "dev": true - }, - "node_modules/accept-language": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", - "integrity": "sha1-9QJfF79lpGaoRYOMz5jNuHfYM4Q=", - "dev": true, - "dependencies": { - "bcp47": "^1.1.2", - "stable": "^0.1.6" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "peerDependencies": { - "ajv": ">=5.0.0" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/anser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.9.tgz", - "integrity": "sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA==", - "dev": true - }, - "node_modules/ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/app-root-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", - "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/assert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", - "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", - "dev": true, - "dependencies": { - "es6-object-assign": "^1.1.0", - "is-nan": "^1.2.1", - "object-is": "^1.0.1", - "util": "^0.12.0" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/ast-types": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.2.tgz", - "integrity": "sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/avvio": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.2.tgz", - "integrity": "sha512-XW2CMCmZaCmCCsIaJaLKxAzPwF37fXi1KGxNOvedOpeisLdmxZnblGc3hpHWYnlP+KOUxZsazh43WXNHgXpbqw==", - "dev": true, - "dependencies": { - "archy": "^1.0.0", - "debug": "^4.0.0", - "fastq": "^1.6.1", - "queue-microtask": "^1.1.2" - } - }, - "node_modules/aws-sdk": { - "version": "2.1077.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1077.0.tgz", - "integrity": "sha512-orJvJROs8hJaQRfHsX7Zl5PxEgrD/uTXyqXz9Yu9Io5VVxzvnOty9oHmvEMSlgTIf1qd01gnev/vpvP1HgzKtw==", - "dev": true, - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-sdk-mock": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.6.2.tgz", - "integrity": "sha512-GRJg8kjRJFLm2aLiPkYSqe/RreHqlqncAeFtWdAbtSxzBdct9EaV6rqSqjyWXKNJG45Rzn2Ojo3F6qVIgkQnSg==", - "dev": true, - "dependencies": { - "aws-sdk": "^2.928.0", - "sinon": "^11.1.1", - "traverse": "^0.6.6" - } - }, - "node_modules/aws-sdk-mock/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/aws-sdk-mock/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-sdk-mock/node_modules/sinon": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", - "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^7.1.2", - "@sinonjs/samsam": "^6.0.2", - "diff": "^5.0.0", - "nise": "^5.1.0", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/aws-sdk-mock/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-sdk/node_modules/uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bcp47": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", - "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/bl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", - "dev": true, - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/bl/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/body-parser/node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/browserify-sign/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "node_modules/bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "dependencies": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/capital-case": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", - "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/change-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", - "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, - "dependencies": { - "camel-case": "^4.1.2", - "capital-case": "^1.0.4", - "constant-case": "^3.0.4", - "dot-case": "^3.0.4", - "header-case": "^2.0.4", - "no-case": "^3.0.4", - "param-case": "^3.0.4", - "pascal-case": "^3.1.2", - "path-case": "^3.0.4", - "sentence-case": "^3.0.4", - "snake-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.1" - } - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "dev": true - }, - "node_modules/cldrjs": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.5.tgz", - "integrity": "sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==", - "dev": true - }, - "node_modules/cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/co-body": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.1.0.tgz", - "integrity": "sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==", - "dependencies": { - "inflation": "^2.0.0", - "qs": "^6.5.2", - "raw-body": "^2.3.3", - "type-is": "^1.6.16" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "node_modules/constant-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", - "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case": "^2.0.2" - } - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dev": true, - "dependencies": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "node_modules/cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", - "dev": true - }, - "node_modules/cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", - "dev": true, - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cookies/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", - "dev": true - }, - "node_modules/cssnano-preset-simple": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssnano-preset-simple/-/cssnano-preset-simple-3.0.0.tgz", - "integrity": "sha512-vxQPeoMRqUT3c/9f0vWeVa2nKQIHFpogtoBvFdW4GQ3IvEJ6uauCP6p3Y5zQDLFcI7/+40FTgX12o7XUL0Ko+w==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001202" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-simple": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssnano-simple/-/cssnano-simple-3.0.0.tgz", - "integrity": "sha512-oU3ueli5Dtwgh0DyeohcIEE00QVfbPR3HzyXdAl89SfnQG3y0/qcpfLVW+jPIh3/rgMZGwuW96rejZGaYE9eUg==", - "dev": true, - "dependencies": { - "cssnano-preset-simple": "^3.0.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/dayjs": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.0.tgz", - "integrity": "sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/domain-browser": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.19.0.tgz", - "integrity": "sha512-fRA+BaAWOR/yr/t7T9E9GJztHPeFjj8U35ajyAjCDtAAnTn1Rc1f6W6VGPJrO1tkQv9zWu+JRof7z6oQtiYVFQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dotenv-json/-/dotenv-json-1.0.0.tgz", - "integrity": "sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ==", - "dev": true - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "node_modules/ejs": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", - "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", - "dev": true, - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/express/node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-json-stringify": { - "version": "2.7.13", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz", - "integrity": "sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==", - "dev": true, - "dependencies": { - "ajv": "^6.11.0", - "deepmerge": "^4.2.2", - "rfdc": "^1.2.0", - "string-similarity": "^4.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/fast-redact": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.0.tgz", - "integrity": "sha512-dir8LOnvialLxiXDPESMDHGp82CHi6ZEYTVkcvdn5d7psdv9ZkkButXrOeXST4aqreIRR+N7CYlsrwFuorurVg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "node_modules/fastify": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.18.1.tgz", - "integrity": "sha512-OA0imy/bQCMzf7LUCb/1JI3ZSoA0Jo0MLpYULxV7gpppOpJ8NBxDp2PQoQ0FDqJevZPb7tlZf5JacIQft8x9yw==", - "dev": true, - "dependencies": { - "@fastify/ajv-compiler": "^1.0.0", - "abstract-logging": "^2.0.0", - "avvio": "^7.1.2", - "fast-json-stringify": "^2.5.2", - "fastify-error": "^0.3.0", - "fastify-warning": "^0.2.0", - "find-my-way": "^4.0.0", - "flatstr": "^1.0.12", - "light-my-request": "^4.2.0", - "pino": "^6.2.1", - "proxy-addr": "^2.0.7", - "readable-stream": "^3.4.0", - "rfdc": "^1.1.4", - "secure-json-parse": "^2.0.0", - "semver": "^7.3.2", - "tiny-lru": "^7.0.0" - } - }, - "node_modules/fastify-error": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", - "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==", - "dev": true - }, - "node_modules/fastify-warning": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", - "integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==", - "deprecated": "This module renamed to process-warning", - "dev": true - }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", - "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/finalhandler/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-my-way": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz", - "integrity": "sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg==", - "dev": true, - "dependencies": { - "fast-decode-uri-component": "^1.0.1", - "fast-deep-equal": "^3.1.3", - "safe-regex2": "^2.0.0", - "semver-store": "^0.3.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "dependencies": { - "is-buffer": "~2.0.3" - }, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat/node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, - "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/formidable": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", - "dev": true, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-orientation": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-orientation/-/get-orientation-1.1.2.tgz", - "integrity": "sha512-/pViTfifW+gBbh/RnlFYHINvELT9Znt+SYyDKAUL6uV6By019AK/s+i9XP4jSwq7lwP38Fd8HVeTxym3+hkwmQ==", - "dev": true, - "dependencies": { - "stream-parser": "^0.3.1" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/globalize": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.7.0.tgz", - "integrity": "sha512-faR46vTIbFCeAemyuc9E6/d7Wrx9k2ae2L60UhakztFg6VuE42gENVJNuPFtt7Sdjrk9m2w8+py7Jj+JTNy59w==", - "dev": true, - "dependencies": { - "cldrjs": "^0.5.4" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "node_modules/growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true, - "engines": { - "node": ">=4.x" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/header-case": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", - "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, - "dependencies": { - "capital-case": "^1.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/http-assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", - "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", - "dev": true, - "dependencies": { - "deep-equal": "~1.0.1", - "http-errors": "~1.8.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/http-status": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.5.0.tgz", - "integrity": "sha512-wcGvY31MpFNHIkUcXHHnvrE4IKYlpvitJw5P/1u892gMBAM46muQ+RH7UN1d+Ntnfx5apnOnVY6vcLmrWHOLwg==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true, - "engines": { - "node": ">=8.12.0" - } - }, - "node_modules/hyperid": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-2.3.1.tgz", - "integrity": "sha512-mIbI7Ymn6MCdODaW1/6wdf5lvvXzmPsARN4zTLakMmcziBOuP4PxCBJvHF6kbAIHX6H4vAELx/pDmt0j6Th5RQ==", - "dev": true, - "dependencies": { - "uuid": "^8.3.2", - "uuid-parse": "^1.1.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-size": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz", - "integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==", - "dev": true, - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/inflation": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", - "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/inflection": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.2.tgz", - "integrity": "sha512-cmZlljCRTBFouT8UzMzrGcVEvkv6D/wBdcdKG7J1QH5cXjtU75Dm+P27v9EKu/Y43UYyCJd1WC4zLebRrC8NBw==", - "dev": true, - "engines": [ - "node >= 0.4.0" - ] - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/invert-kv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", - "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sindresorhus/invert-kv?sponsor=1" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz", - "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "27.0.0-next.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.0-next.5.tgz", - "integrity": "sha512-mk0umAQ5lT+CaOJ+Qp01N6kz48sJG2kr2n1rX0koqKf6FIygQV0qLOdN9SCYID4IVeSigDOcPeGLozdMLYfb5g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/joi": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", - "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/jose": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", - "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", - "dependencies": { - "@panva/asn1.js": "^1.0.0" - }, - "engines": { - "node": ">=10.13.0 < 13 || >=13.7.0" - }, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "dev": true, - "dependencies": { - "xmlcreate": "^2.0.4" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "peer": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-merge-patch": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-merge-patch/-/json-merge-patch-1.0.2.tgz", - "integrity": "sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/json-schema-compare": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", - "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", - "dev": true, - "dependencies": { - "lodash": "^4.17.4" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "peer": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5/node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", - "dev": true - }, - "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jwks-rsa": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", - "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", - "dependencies": { - "@types/express-jwt": "0.0.42", - "debug": "^4.3.2", - "jose": "^2.0.5", - "limiter": "^1.1.5", - "lru-memoizer": "^2.1.4" - }, - "engines": { - "node": ">=10 < 13 || >=14" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dev": true, - "dependencies": { - "tsscmp": "1.0.6" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/koa": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.4.tgz", - "integrity": "sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==", - "dev": true, - "dependencies": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.8.0", - "debug": "^4.3.2", - "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" - }, - "engines": { - "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" - } - }, - "node_modules/koa-compose": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", - "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", - "dev": true - }, - "node_modules/koa-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", - "dev": true, - "dependencies": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/koa/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/lambda-event-mock": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lambda-event-mock/-/lambda-event-mock-1.5.0.tgz", - "integrity": "sha512-vx1d+vZqi7FF6B3+mAfHnY/6Tlp6BheL2ta0MJS0cIRB3Rc4I5cviHTkiJxHdE156gXx3ZjlQRJrS4puXvtrhA==", - "dev": true, - "dependencies": { - "@extra-number/significant-digits": "^1.1.1", - "clone-deep": "^4.0.1", - "uuid": "^3.3.3", - "vandium-utils": "^1.2.0" - }, - "engines": { - "node": ">=12.13" - } - }, - "node_modules/lambda-event-mock/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/lambda-event-mock/node_modules/vandium-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vandium-utils/-/vandium-utils-1.2.0.tgz", - "integrity": "sha1-RHNd5LdkGgXeWevpRfF05YLbT1k=", - "dev": true - }, - "node_modules/lambda-leak": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lambda-leak/-/lambda-leak-2.0.0.tgz", - "integrity": "sha1-dxmF02KEh/boha+uK1RRDc+yzX4=", - "dev": true, - "engines": { - "node": ">=6.10.0" - } - }, - "node_modules/lambda-tester": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lambda-tester/-/lambda-tester-4.0.1.tgz", - "integrity": "sha512-ft6XHk84B6/dYEzyI3anKoGWz08xQ5allEHiFYDUzaYTymgVK7tiBkCEbuWx+MFvH7OpFNsJXVtjXm0X8iH3Iw==", - "dev": true, - "dependencies": { - "app-root-path": "^3.0.0", - "dotenv": "^8.0.0", - "dotenv-json": "^1.0.0", - "lambda-event-mock": "^1.5.0", - "lambda-leak": "^2.0.0", - "semver": "^6.1.1", - "uuid": "^3.3.3", - "vandium-utils": "^2.0.0" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/lambda-tester/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/lambda-tester/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/lcid": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", - "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", - "dev": true, - "dependencies": { - "invert-kv": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/libphonenumber-js": { - "version": "1.9.49", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.49.tgz", - "integrity": "sha512-/wEOIONcVboFky+lWlCaF7glm1FhBz11M5PHeCApA+xDdVfmhKjHktHS8KjyGxouV5CSXIr4f3GvLSpJa4qMSg==" - }, - "node_modules/light-my-request": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.8.0.tgz", - "integrity": "sha512-C2XESrTRsZnI59NSQigOsS6IuTxpj8OhSBvZS9fhgBMsamBsAuWN1s4hj/nCi8EeZcyAA6xbROhsZy7wKdfckg==", - "dev": true, - "dependencies": { - "ajv": "^8.1.0", - "cookie": "^0.4.0", - "process-warning": "^1.0.0", - "set-cookie-parser": "^2.4.1" - } - }, - "node_modules/light-my-request/node_modules/ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/light-my-request/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "node_modules/loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/loader-utils/node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "node_modules/log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "dependencies": { - "chalk": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/loopback-connector": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-5.0.1.tgz", - "integrity": "sha512-aSPT6x5WZdoW9ylyNE4CxGqFbIqC9cSEZJwWkCincso27PXlZPj52POoF6pgxug9mkH7MrbXuP3SSDLkLq5oQQ==", - "dev": true, - "dependencies": { - "async": "^3.2.0", - "bluebird": "^3.7.2", - "debug": "^4.1.1", - "msgpack5": "^4.2.0", - "strong-globalize": "^6.0.4", - "uuid": "^8.3.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/loopback-datasource-juggler": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-4.26.0.tgz", - "integrity": "sha512-/R40jUGDrnRBgTh121L4Y7sHDF0KxbgSAN4gLJKp8xGNQ6KpkSQyqZkmap98eN7B75RES78DS3MGghsYMvAJ3Q==", - "dev": true, - "dependencies": { - "async": "^3.1.0", - "change-case": "^4.1.1", - "debug": "^4.1.0", - "depd": "^2.0.0", - "inflection": "^1.6.0", - "lodash": "^4.17.11", - "loopback-connector": "^5.0.0", - "minimatch": "^3.0.3", - "qs": "^6.5.0", - "shortid": "^2.2.6", - "strong-globalize": "^6.0.5", - "traverse": "^0.6.6", - "uuid": "^8.3.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/loopback-datasource-juggler/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", - "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", - "dependencies": { - "pseudomap": "^1.0.1", - "yallist": "^2.0.0" - } - }, - "node_modules/lru-memoizer": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", - "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "~4.0.0" - } - }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "dev": true - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/marked": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", - "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", - "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mem": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", - "integrity": "sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^2.1.0", - "p-is-promise": "^2.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dependencies": { - "mime-db": "1.51.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", - "dev": true, - "dependencies": { - "minimist": "0.0.8" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", - "dev": true, - "dependencies": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/mocha/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/msgpack5": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.5.1.tgz", - "integrity": "sha512-zC1vkcliryc4JGlL6OfpHumSYUHWFGimSI+OgfRCjTFLmKA2/foR9rMTOhWiqfOrfxJOctrpWPvrppf8XynJxw==", - "dev": true, - "dependencies": { - "bl": "^2.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.3.6", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/msgpack5/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/msgpack5/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/multimatch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", - "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", - "dev": true, - "dependencies": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/native-url": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/native-url/-/native-url-0.3.4.tgz", - "integrity": "sha512-6iM8R99ze45ivyH8vybJ7X0yekIcPf5GgLV5K0ENCbmRcaRIDoj37BC8iLEmaaBfqqb8enuZ5p0uhY+lVAbAcA==", - "dev": true, - "dependencies": { - "querystring": "^0.2.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/next": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/next/-/next-11.1.3.tgz", - "integrity": "sha512-ud/gKmnKQ8wtHC+pd1ZiqPRa7DdgulPkAk94MbpsspfNliwZkYs9SIYWhlLSyg+c661LzdUI2nZshvrtggSYWA==", - "dev": true, - "dependencies": { - "@babel/runtime": "7.15.3", - "@hapi/accept": "5.0.2", - "@next/env": "11.1.3", - "@next/polyfill-module": "11.1.3", - "@next/react-dev-overlay": "11.1.3", - "@next/react-refresh-utils": "11.1.3", - "@node-rs/helper": "1.2.1", - "assert": "2.0.0", - "ast-types": "0.13.2", - "browserify-zlib": "0.2.0", - "browserslist": "4.16.6", - "buffer": "5.6.0", - "caniuse-lite": "^1.0.30001228", - "chalk": "2.4.2", - "chokidar": "3.5.1", - "constants-browserify": "1.0.0", - "crypto-browserify": "3.12.0", - "cssnano-simple": "3.0.0", - "domain-browser": "4.19.0", - "encoding": "0.1.13", - "etag": "1.8.1", - "find-cache-dir": "3.3.1", - "get-orientation": "1.1.2", - "https-browserify": "1.0.0", - "image-size": "1.0.0", - "jest-worker": "27.0.0-next.5", - "native-url": "0.3.4", - "node-fetch": "2.6.1", - "node-html-parser": "1.4.9", - "node-libs-browser": "^2.2.1", - "os-browserify": "0.3.0", - "p-limit": "3.1.0", - "path-browserify": "1.0.1", - "pnp-webpack-plugin": "1.6.4", - "postcss": "8.2.15", - "process": "0.11.10", - "querystring-es3": "0.2.1", - "raw-body": "2.4.1", - "react-is": "17.0.2", - "react-refresh": "0.8.3", - "stream-browserify": "3.0.0", - "stream-http": "3.1.1", - "string_decoder": "1.3.0", - "styled-jsx": "4.0.1", - "timers-browserify": "2.0.12", - "tty-browserify": "0.0.1", - "use-subscription": "1.5.1", - "util": "0.12.4", - "vm-browserify": "1.1.2", - "watchpack": "2.1.1" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=12.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "11.1.3", - "@next/swc-darwin-x64": "11.1.3", - "@next/swc-linux-x64-gnu": "11.1.3", - "@next/swc-win32-x64-msvc": "11.1.3" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-test-api-route-handler": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/next-test-api-route-handler/-/next-test-api-route-handler-3.1.8.tgz", - "integrity": "sha512-8o+TjtlQdvLv7YmR5NOl+AQCM/tEZqB1mvqBeGFQscuGtL+42fOrOsDFv2QsFUWieUMKv7j7NqOmYMpJRPu3/w==", - "dev": true, - "dependencies": { - "cookie": "^0.5.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "next": ">=9" - } - }, - "node_modules/next-test-api-route-handler/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/next-test-api-route-handler/node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/next/node_modules/buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "node_modules/next/node_modules/http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/next/node_modules/raw-body": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", - "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", - "dev": true, - "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.3", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/next/node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, - "node_modules/nise/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/nock": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/nock/-/nock-11.7.0.tgz", - "integrity": "sha512-7c1jhHew74C33OBeRYyQENT+YXQiejpwIrEjinh6dRurBae+Ei4QjeUaPlkptIF0ZacEiVCnw8dWaxqepkiihg==", - "dev": true, - "dependencies": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.13", - "mkdirp": "^0.5.0", - "propagate": "^2.0.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "dev": true, - "dependencies": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - } - }, - "node_modules/node-environment-flags/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true, - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-html-parser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.4.9.tgz", - "integrity": "sha512-UVcirFD1Bn0O+TSmloHeHqZZCxHjvtIeGdVdGMhyZ8/PWlEiZaZ5iJzR189yKZr8p0FXN58BUeC7RHRkf/KYGw==", - "dev": true, - "dependencies": { - "he": "1.2.0" - } - }, - "node_modules/node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "dependencies": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - } - }, - "node_modules/node-libs-browser/node_modules/assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "util": "0.10.3" - } - }, - "node_modules/node-libs-browser/node_modules/assert/node_modules/inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "node_modules/node-libs-browser/node_modules/assert/node_modules/util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "dependencies": { - "inherits": "2.0.1" - } - }, - "node_modules/node-libs-browser/node_modules/domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true, - "engines": { - "node": ">=0.4", - "npm": ">=1.2" - } - }, - "node_modules/node-libs-browser/node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/node-libs-browser/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "node_modules/node-libs-browser/node_modules/path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "node_modules/node-libs-browser/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "node_modules/node-libs-browser/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/node-libs-browser/node_modules/stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "dependencies": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/node-libs-browser/node_modules/stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/node-libs-browser/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/node-libs-browser/node_modules/tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "node_modules/node-libs-browser/node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/node-libs-browser/node_modules/util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/node-releases": { - "version": "1.1.77", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", - "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==", - "dev": true - }, - "node_modules/nodemailer": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.3.tgz", - "integrity": "sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", - "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", - "dev": true - }, - "node_modules/openapi3-ts": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", - "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", - "dev": true, - "dependencies": { - "yaml": "^1.10.2" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "node_modules/os-locale": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz", - "integrity": "sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==", - "dev": true, - "dependencies": { - "execa": "^4.0.0", - "lcid": "^3.0.0", - "mem": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dev": true, - "dependencies": { - "p-timeout": "^3.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "node_modules/path-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", - "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", - "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true, - "peer": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", - "dev": true, - "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==", - "dev": true - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/platform": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", - "dev": true - }, - "node_modules/pnp-webpack-plugin": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", - "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", - "dev": true, - "dependencies": { - "ts-pnp": "^1.1.6" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/postcss": { - "version": "8.2.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz", - "integrity": "sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==", - "dev": true, - "dependencies": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map": "^0.6.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/pretty-quick": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.1.3.tgz", - "integrity": "sha512-kOCi2FJabvuh1as9enxYmrnBC6tVMoVOenMaBqRfsvBHB0cbpYHjdQEpSglpASDFEXVwplpcGR4CLEaisYAFcA==", - "dev": true, - "dependencies": { - "chalk": "^3.0.0", - "execa": "^4.0.0", - "find-up": "^4.1.0", - "ignore": "^5.1.4", - "mri": "^1.1.5", - "multimatch": "^4.0.0" - }, - "bin": { - "pretty-quick": "bin/pretty-quick.js" - }, - "engines": { - "node": ">=10.13" - }, - "peerDependencies": { - "prettier": ">=2.0.0" - } - }, - "node_modules/pretty-quick/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-quick/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/pretty-quick/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/pretty-quick/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-quick/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-quick/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "dev": true - }, - "node_modules/propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "dev": true, - "dependencies": { - "inherits": "~2.0.3" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "dev": true - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "node_modules/react-refresh": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", - "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true - }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "node_modules/ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", - "dev": true, - "dependencies": { - "ret": "~0.2.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", - "dev": true - }, - "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, - "node_modules/secure-json-parse": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", - "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==", - "dev": true - }, - "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", - "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==", - "dev": true - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/send/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/send/node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/sentence-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", - "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "node_modules/set-cookie-parser": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", - "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==", - "dev": true - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true - }, - "node_modules/shiki": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz", - "integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.0.0", - "vscode-oniguruma": "^1.6.1", - "vscode-textmate": "5.2.0" - } - }, - "node_modules/shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", - "dev": true, - "dependencies": { - "nanoid": "^2.1.0" - } - }, - "node_modules/shortid/node_modules/nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", - "dev": true - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/sinon": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.0.tgz", - "integrity": "sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^6.1.1", - "diff": "^5.0.0", - "nise": "^5.1.1", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon/node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/sinon/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sinon/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", - "dev": true, - "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } - }, - "node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/source-map/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "node_modules/source-map/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "node_modules/stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dev": true, - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true, - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-http": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.1.tgz", - "integrity": "sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg==", - "dev": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "node_modules/stream-parser": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", - "integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=", - "dev": true, - "dependencies": { - "debug": "2" - } - }, - "node_modules/stream-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/stream-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", - "dev": true - }, - "node_modules/string-similarity": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", - "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", - "dev": true - }, - "node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strong-error-handler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-4.0.0.tgz", - "integrity": "sha512-Ki59WSOfSEod6IkDUB4uf9+DwkCLQRbEdYqen167I/zyPps9x9gS+UzhLZOcer58RA6iFmoGg/+CN/x5d+Cv3Q==", - "dev": true, - "dependencies": { - "@types/express": "^4.16.0", - "accepts": "^1.3.3", - "debug": "^4.1.1", - "ejs": "^3.1.3", - "fast-safe-stringify": "^2.0.6", - "http-status": "^1.1.2", - "js2xmlparser": "^4.0.0", - "strong-globalize": "^6.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/strong-globalize": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-6.0.5.tgz", - "integrity": "sha512-7nfUli41TieV9/TSc0N62ve5Q4nfrpy/T0nNNy6TyD3vst79QWmeylCyd3q1gDxh8dqGEtabLNCdPQP1Iuvecw==", - "dev": true, - "dependencies": { - "accept-language": "^3.0.18", - "debug": "^4.2.0", - "globalize": "^1.6.0", - "lodash": "^4.17.20", - "md5": "^2.3.0", - "mkdirp": "^1.0.4", - "os-locale": "^5.0.0", - "yamljs": "^0.3.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/strong-globalize/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/styled-jsx": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-4.0.1.tgz", - "integrity": "sha512-Gcb49/dRB1k8B4hdK8vhW27Rlb2zujCk1fISrizCcToIs+55B4vmUM0N9Gi4nnVfFZWe55jRdWpAqH1ldAKWvQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-jsx": "7.14.5", - "@babel/types": "7.15.0", - "convert-source-map": "1.7.0", - "loader-utils": "1.2.3", - "source-map": "0.7.3", - "string-hash": "1.1.3", - "stylis": "3.5.4", - "stylis-rule-sheet": "0.0.10" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || 18.x.x" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - } - } - }, - "node_modules/styled-jsx/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/stylis": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "dev": true - }, - "node_modules/stylis-rule-sheet": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", - "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", - "dev": true, - "peerDependencies": { - "stylis": "^3.5.0" - } - }, - "node_modules/superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . Thanks to @shadowgate15, @spence-s, and @niftylettuce. Superagent is sponsored by Forward Email at .", - "dev": true, - "dependencies": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/superagent/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/superagent/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/supertest": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", - "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", - "dev": true, - "dependencies": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/supertokens-js-override": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/supertokens-js-override/-/supertokens-js-override-0.0.4.tgz", - "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==" - }, - "node_modules/supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "dependencies": { - "setimmediate": "^1.0.4" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tiny-lru": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", - "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=", - "dev": true - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", - "dev": true - }, - "node_modules/ts-pnp": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", - "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "dev": true, - "engines": { - "node": ">=0.6.x" - } - }, - "node_modules/tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true - }, - "node_modules/twilio": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.7.2.tgz", - "integrity": "sha512-sTdEwAhkDzoDoXE3i83F/CdZegZ5O1FPDY0hAnJmGr/TjDqGl+Q6WzjC0+9cTmQnjCaDg6H4L97UZeJLFFEh3A==", - "dependencies": { - "axios": "^0.26.1", - "dayjs": "^1.8.29", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.0", - "qs": "^6.9.4", - "scmp": "^2.1.0", - "url-parse": "^1.5.9", - "xmlbuilder": "^13.0.2" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/twilio/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/twilio/node_modules/xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedoc": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", - "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", - "dev": true, - "dependencies": { - "glob": "^7.2.0", - "lunr": "^2.3.9", - "marked": "^4.0.10", - "minimatch": "^3.0.4", - "shiki": "^0.10.0" - }, - "bin": { - "typedoc": "bin/typedoc" - }, - "engines": { - "node": ">= 12.10.0" - }, - "peerDependencies": { - "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x" - } - }, - "node_modules/typedoc/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", - "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/upper-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", - "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "dev": true, - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "node_modules/use-subscription": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz", - "integrity": "sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/util": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", - "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "safe-buffer": "^5.1.2", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/uuid-parse": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", - "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", - "dev": true - }, - "node_modules/validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vandium-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vandium-utils/-/vandium-utils-2.0.0.tgz", - "integrity": "sha512-XWbQ/0H03TpYDXk8sLScBEZpE7TbA0CHDL6/Xjt37IBYKLsHUQuBlL44ttAUs9zoBOLFxsW7HehXcuWCNyqOxQ==", - "dev": true, - "engines": { - "node": ">=10.16" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verify-apple-id-token": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/verify-apple-id-token/-/verify-apple-id-token-3.0.1.tgz", - "integrity": "sha512-q91pG1e52TpEzXldMirWYNWcSQC4WuzgG0y/ZnBhzjfk0pSxi4YlGh5OTVRlodBenayGHfSDn5VseG9QDuqOew==", - "dependencies": { - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.0.0" - } - }, - "node_modules/verify-apple-id-token/node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/verify-apple-id-token/node_modules/jose": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.4.tgz", - "integrity": "sha512-94FdcR8felat4vaTJyL/WVdtlWLlsnLMZP8v+A0Vru18K3bQ22vn7TtpVh3JlgBFNIlYOUlGqwp/MjRPOnIyCQ==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/verify-apple-id-token/node_modules/jwks-rsa": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", - "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", - "dependencies": { - "@types/express": "^4.17.14", - "@types/jsonwebtoken": "^9.0.0", - "debug": "^4.3.4", - "jose": "^4.10.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.1.4" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "node_modules/vscode-oniguruma": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz", - "integrity": "sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ==", - "dev": true - }, - "node_modules/vscode-textmate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", - "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==", - "dev": true - }, - "node_modules/watchpack": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz", - "integrity": "sha512-Oo7LXCmc1eE1AjyuSBmtC3+Wy4HcV8PxWh2kP6fOl8yTlNS7r0K9l1ao2lrrUza7V39Y3D/BbJgY8VeSlc5JKw==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "node_modules/which-typed-array": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", - "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" - } - }, - "node_modules/yamljs/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "dev": true, - "dependencies": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - } - }, - "node_modules/yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "dev": true, - "dependencies": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/yargs-unparser/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "node_modules/yargs-unparser/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs-unparser/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs-unparser/node_modules/lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs-unparser/node_modules/os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-unparser/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs-unparser/node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "node_modules/yargs-unparser/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/yargs-unparser/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs-unparser/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs-unparser/node_modules/yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "dependencies": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "node_modules/yargs-unparser/node_modules/yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/yargs/node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/yargs/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/ylru": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", - "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", - "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "dev": true, - "peer": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.0" - } - }, - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", - "dev": true, - "peer": true - }, - "@babel/core": { - "version": "7.17.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.5.tgz", - "integrity": "sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA==", - "dev": true, - "peer": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.17.2", - "@babel/parser": "^7.17.3", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "peer": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "peer": true - } - } - }, - "@babel/generator": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", - "integrity": "sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "peer": true - } - } - }, - "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, - "peer": true, - "requires": { - "@babel/compat-data": "^7.16.4", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.17.5", - "semver": "^6.3.0" - }, - "dependencies": { - "browserslist": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.3.tgz", - "integrity": "sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==", - "dev": true, - "peer": true, - "requires": { - "caniuse-lite": "^1.0.30001312", - "electron-to-chromium": "^1.4.71", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" - } - }, - "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", - "dev": true, - "peer": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "peer": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", - "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-function-name": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", - "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-get-function-arity": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-get-function-arity": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", - "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-hoist-variables": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", - "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-module-imports": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", - "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-plugin-utils": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", - "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", - "dev": true - }, - "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", - "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", - "dev": true, - "peer": true, - "requires": { - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/helper-validator-identifier": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", - "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, - "peer": true - }, - "@babel/helpers": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.2.tgz", - "integrity": "sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", - "@babel/types": "^7.17.0" - }, - "dependencies": { - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.3.tgz", - "integrity": "sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==", - "dev": true, - "peer": true - }, - "@babel/plugin-syntax-jsx": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", - "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/runtime": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz", - "integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@babel/template": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", - "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", - "dev": true, - "peer": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "peer": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", - "dev": true, - "peer": true, - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "peer": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, - "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "peer": true, - "requires": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - } - } - } - }, - "@babel/types": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", - "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.9", - "to-fast-properties": "^2.0.0" - } - }, - "@extra-number/significant-digits": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@extra-number/significant-digits/-/significant-digits-1.3.9.tgz", - "integrity": "sha512-E5PY/bCwrNqEHh4QS6AQBinLZ+sxM1lT8tsSVYk8VwhWIPp6fCU/BMRVq0V8iJ8LwS3FHmaA4vUzb78s4BIIyA==", - "dev": true - }, - "@fastify/ajv-compiler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", - "integrity": "sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg==", - "dev": true, - "requires": { - "ajv": "^6.12.6" - } - }, - "@hapi/accept": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-5.0.2.tgz", - "integrity": "sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/ammo": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-5.0.1.tgz", - "integrity": "sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/b64": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-5.0.0.tgz", - "integrity": "sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/bounce": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-2.0.0.tgz", - "integrity": "sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/bourne": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.0.0.tgz", - "integrity": "sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==", - "dev": true - }, - "@hapi/call": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@hapi/call/-/call-8.0.1.tgz", - "integrity": "sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/catbox": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-11.1.1.tgz", - "integrity": "sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/podium": "4.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/catbox-memory": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz", - "integrity": "sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/content": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@hapi/content/-/content-5.0.2.tgz", - "integrity": "sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x" - } - }, - "@hapi/cryptiles": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-5.1.0.tgz", - "integrity": "sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x" - } - }, - "@hapi/file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/file/-/file-2.0.0.tgz", - "integrity": "sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==", - "dev": true - }, - "@hapi/hapi": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-20.2.1.tgz", - "integrity": "sha512-OXAU+yWLwkMfPFic+KITo+XPp6Oxpgc9WUH+pxXWcTIuvWbgco5TC/jS8UDvz+NFF5IzRgF2CL6UV/KLdQYUSQ==", - "dev": true, - "requires": { - "@hapi/accept": "^5.0.1", - "@hapi/ammo": "^5.0.1", - "@hapi/boom": "^9.1.0", - "@hapi/bounce": "^2.0.0", - "@hapi/call": "^8.0.0", - "@hapi/catbox": "^11.1.1", - "@hapi/catbox-memory": "^5.0.0", - "@hapi/heavy": "^7.0.1", - "@hapi/hoek": "^9.0.4", - "@hapi/mimos": "^6.0.0", - "@hapi/podium": "^4.1.1", - "@hapi/shot": "^5.0.5", - "@hapi/somever": "^3.0.0", - "@hapi/statehood": "^7.0.3", - "@hapi/subtext": "^7.0.3", - "@hapi/teamwork": "^5.1.0", - "@hapi/topo": "^5.0.0", - "@hapi/validate": "^1.1.1" - } - }, - "@hapi/heavy": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-7.0.1.tgz", - "integrity": "sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/hoek": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", - "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==", - "dev": true - }, - "@hapi/iron": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-6.0.0.tgz", - "integrity": "sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==", - "dev": true, - "requires": { - "@hapi/b64": "5.x.x", - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/cryptiles": "5.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/mimos": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-6.0.0.tgz", - "integrity": "sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x", - "mime-db": "1.x.x" - } - }, - "@hapi/nigel": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-4.0.2.tgz", - "integrity": "sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.4", - "@hapi/vise": "^4.0.0" - } - }, - "@hapi/pez": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-5.0.3.tgz", - "integrity": "sha512-mpikYRJjtrbJgdDHG/H9ySqYqwJ+QU/D7FXsYciS9P7NYBXE2ayKDAy3H0ou6CohOCaxPuTV4SZ0D936+VomHA==", - "dev": true, - "requires": { - "@hapi/b64": "5.x.x", - "@hapi/boom": "9.x.x", - "@hapi/content": "^5.0.2", - "@hapi/hoek": "9.x.x", - "@hapi/nigel": "4.x.x" - } - }, - "@hapi/podium": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-4.1.3.tgz", - "integrity": "sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x", - "@hapi/teamwork": "5.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/shot": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-5.0.5.tgz", - "integrity": "sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/somever": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-3.0.1.tgz", - "integrity": "sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w==", - "dev": true, - "requires": { - "@hapi/bounce": "2.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/statehood": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-7.0.3.tgz", - "integrity": "sha512-pYB+pyCHkf2Amh67QAXz7e/DN9jcMplIL7Z6N8h0K+ZTy0b404JKPEYkbWHSnDtxLjJB/OtgElxocr2fMH4G7w==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/bounce": "2.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/cryptiles": "5.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/iron": "6.x.x", - "@hapi/validate": "1.x.x" - } - }, - "@hapi/subtext": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-7.0.3.tgz", - "integrity": "sha512-CekDizZkDGERJ01C0+TzHlKtqdXZxzSWTOaH6THBrbOHnsr3GY+yiMZC+AfNCypfE17RaIakGIAbpL2Tk1z2+A==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/content": "^5.0.2", - "@hapi/file": "2.x.x", - "@hapi/hoek": "9.x.x", - "@hapi/pez": "^5.0.1", - "@hapi/wreck": "17.x.x" - } - }, - "@hapi/teamwork": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-5.1.0.tgz", - "integrity": "sha512-llqoQTrAJDTXxG3c4Kz/uzhBS1TsmSBa/XG5SPcVXgmffHE1nFtyLIK0hNJHCB3EuBKT84adzd1hZNY9GJLWtg==", - "dev": true - }, - "@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@hapi/validate": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-1.1.3.tgz", - "integrity": "sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0" - } - }, - "@hapi/vise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-4.0.0.tgz", - "integrity": "sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==", - "dev": true, - "requires": { - "@hapi/hoek": "9.x.x" - } - }, - "@hapi/wreck": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-17.1.0.tgz", - "integrity": "sha512-nx6sFyfqOpJ+EFrHX+XWwJAxs3ju4iHdbB/bwR8yTNZOiYmuhA8eCe7lYPtYmb4j7vyK/SlbaQsmTtUrMvPEBw==", - "dev": true, - "requires": { - "@hapi/boom": "9.x.x", - "@hapi/bourne": "2.x.x", - "@hapi/hoek": "9.x.x" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", - "dev": true, - "peer": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", - "dev": true, - "peer": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", - "dev": true, - "peer": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@koa/router": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-10.1.1.tgz", - "integrity": "sha512-ORNjq5z4EmQPriKbR0ER3k4Gh7YGNhWDL7JBW+8wXDrHLbWYKYSJaOJ9aN06npF5tbTxe2JBOsurpJDAvjiXKw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.1.0" - } - }, - "@loopback/context": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/@loopback/context/-/context-3.18.0.tgz", - "integrity": "sha512-PKx0rTguqBj6mUHBbEHLF031MnP6KiSkMLE4E8Hpy2KPJxG97HUT2ZUACHCP6qm8yS9spWQQ6g72VYAWxDrN+g==", - "dev": true, - "requires": { - "@loopback/metadata": "^3.3.4", - "@types/debug": "^4.1.7", - "debug": "^4.3.2", - "hyperid": "^2.3.1", - "p-event": "^4.2.0", - "tslib": "^2.3.1", - "uuid": "^8.3.2" - } - }, - "@loopback/core": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@loopback/core/-/core-2.16.2.tgz", - "integrity": "sha512-KtkNv6HIh8TFBOxTkfPp/BQbVqjDsGef/DtbNHH1ZHs3gSbofhkZs3IqQdYQzpkUq71mQjz5RJ/yUYY5Sqva9w==", - "dev": true, - "requires": { - "@loopback/context": "^3.17.1", - "debug": "^4.3.1", - "tslib": "^2.3.0" - } - }, - "@loopback/filter": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@loopback/filter/-/filter-1.5.4.tgz", - "integrity": "sha512-kfdCgSy0YoAFNYXJOpag5uJnlErYcROIeJqeAglkwOa3hSw2BYIudurU8hoqsiOBIGhI5BF4A3S8u4q089xWlg==", - "dev": true, - "requires": { - "tslib": "^2.3.1" - } - }, - "@loopback/http-server": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@loopback/http-server/-/http-server-2.5.4.tgz", - "integrity": "sha512-M7w+4AEhwDn7q00soCe8yYQDUS+n87ppuXQ1rJ9a1b9TdnEh+7nPFVrVpwiEKBGyVGIJWDq5BMSZYo1zMIPFUA==", - "dev": true, - "requires": { - "debug": "^4.3.2", - "stoppable": "^1.1.0", - "tslib": "^2.3.1" - } - }, - "@loopback/metadata": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-3.3.4.tgz", - "integrity": "sha512-FISs8OVYKB+wmL0VZdsDZzMOc/KC6anOf3ORpFRO2Mgl9dKCOD8IELKc8r/nr2kyD4r7/pjr5GfLy4nirS1vnQ==", - "dev": true, - "requires": { - "debug": "^4.3.2", - "lodash": "^4.17.21", - "reflect-metadata": "^0.1.13", - "tslib": "^2.3.1" - } - }, - "@loopback/repository": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-3.7.1.tgz", - "integrity": "sha512-q9vpgQ5MSZqI/ww2TTuDy0Y34NZaapS0+4ZKcwVgwH0XJFxgGwznc5W0l1Esu1lplijejpza4ItKwnlGmvYcJg==", - "dev": true, - "requires": { - "@loopback/filter": "^1.5.2", - "@types/debug": "^4.1.5", - "debug": "^4.3.1", - "lodash": "^4.17.21", - "loopback-datasource-juggler": "^4.26.0", - "tslib": "^2.3.0" - } - }, - "@loopback/rest": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-9.3.0.tgz", - "integrity": "sha512-Qwn5WXctQ2AV6Duze/x7ks9Tb/Oy6FSs0Hhyuhzz7aassKbu1m/GiJLB7bvpJVUN+qVZ2+vQZZ6Y3F+znzH4og==", - "dev": true, - "requires": { - "@loopback/express": "^3.3.0", - "@loopback/http-server": "^2.5.0", - "@loopback/openapi-v3": "^5.3.0", - "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.0", - "@types/body-parser": "^1.19.0", - "@types/cors": "^2.8.10", - "@types/express": "^4.17.11", - "@types/express-serve-static-core": "^4.17.19", - "@types/http-errors": "^1.8.0", - "@types/on-finished": "^2.3.1", - "@types/serve-static": "1.13.9", - "@types/type-is": "^1.6.3", - "ajv": "^6.12.6", - "ajv-errors": "^1.0.1", - "ajv-keywords": "^3.5.2", - "body-parser": "^1.19.0", - "cors": "^2.8.5", - "debug": "^4.3.1", - "express": "^4.17.1", - "http-errors": "^1.8.0", - "js-yaml": "^4.1.0", - "json-schema-compare": "^0.2.2", - "lodash": "^4.17.21", - "on-finished": "^2.3.0", - "path-to-regexp": "^6.2.0", - "qs": "^6.10.1", - "strong-error-handler": "^4.0.0", - "tslib": "^2.2.0", - "type-is": "^1.6.18", - "validator": "^13.6.0" - }, - "dependencies": { - "@loopback/express": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@loopback/express/-/express-3.3.4.tgz", - "integrity": "sha512-y+7fu/aXGp7+5QhEnKhwmI/vKLAQtsyvEXTiT8stbj4VHWvNbUbYVwfud5IOeH7d+lhxlNQ5aEJ7IDwoM8xD6Q==", - "dev": true, - "requires": { - "@loopback/http-server": "^2.5.4", - "@types/body-parser": "^1.19.1", - "@types/express": "^4.17.13", - "@types/express-serve-static-core": "^4.17.24", - "@types/http-errors": "^1.8.1", - "body-parser": "^1.19.0", - "debug": "^4.3.2", - "express": "^4.17.1", - "http-errors": "^1.8.0", - "on-finished": "^2.3.0", - "toposort": "^2.0.2", - "tslib": "^2.3.1" - } - }, - "@loopback/openapi-v3": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@loopback/openapi-v3/-/openapi-v3-5.3.1.tgz", - "integrity": "sha512-MBVamgxDDbgQQlQjIxSOnaVXLx6plxzn3e8CW8YbNc3TNiS1P8EFa5vNBp8wIzSDTeEd3ic6qzUxCUZIICiFNA==", - "dev": true, - "requires": { - "@loopback/repository-json-schema": "^3.4.1", - "debug": "^4.3.1", - "http-status": "^1.5.0", - "json-merge-patch": "^1.0.1", - "lodash": "^4.17.21", - "openapi3-ts": "^2.0.1", - "tslib": "^2.2.0" - }, - "dependencies": { - "@loopback/repository-json-schema": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@loopback/repository-json-schema/-/repository-json-schema-3.4.1.tgz", - "integrity": "sha512-E9UKegav+8Bp0MLPQu33c7tWUmWbnKARy0Uu2m7nvP3e3t3WOwB8U9hMjX/wBOhJ4UFJCXAXlq1MulQ/R3dyTw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.7", - "debug": "^4.3.1", - "tslib": "^2.2.0" - } - } - } - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - } - } - }, - "@napi-rs/triples": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.1.0.tgz", - "integrity": "sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w==", - "dev": true - }, - "@next/env": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-11.1.3.tgz", - "integrity": "sha512-5+vaeooJuWmICSlmVaAC8KG3O8hwKasACVfkHj58xQuCB5SW0TKW3hWxgxkBuefMBn1nM0yEVPKokXCsYjBtng==", - "dev": true - }, - "@next/polyfill-module": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/polyfill-module/-/polyfill-module-11.1.3.tgz", - "integrity": "sha512-7yr9cr4a0SrBoVE8psxXWK1wTFc8UzsY8Wc2cWGL7qA0hgtqACHaXC47M1ByJB410hFZenGrpE+KFaT1unQMyw==", - "dev": true - }, - "@next/react-dev-overlay": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/react-dev-overlay/-/react-dev-overlay-11.1.3.tgz", - "integrity": "sha512-zIwtMliSUR+IKl917ToFNB+0fD7bI5kYMdjHU/UEKpfIXAZPnXRHHISCvPDsczlr+bRsbjlUFW1CsNiuFedeuQ==", - "dev": true, - "requires": { - "@babel/code-frame": "7.12.11", - "anser": "1.4.9", - "chalk": "4.0.0", - "classnames": "2.2.6", - "css.escape": "1.5.1", - "data-uri-to-buffer": "3.0.1", - "platform": "1.3.6", - "shell-quote": "1.7.2", - "source-map": "0.8.0-beta.0", - "stacktrace-parser": "0.1.10", - "strip-ansi": "6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@next/react-refresh-utils": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/react-refresh-utils/-/react-refresh-utils-11.1.3.tgz", - "integrity": "sha512-144kD8q2nChw67V3AJJlPQ6NUJVFczyn10bhTynn9o2rY5DEnkzuBipcyMuQl2DqfxMkV7sn+yOCOYbrLCk9zg==", - "dev": true, - "requires": {} - }, - "@next/swc-darwin-arm64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.3.tgz", - "integrity": "sha512-TwP4krjhs+uU9pesDYCShEXZrLSbJr78p12e7XnLBBaNf20SgWLlVmQUT9gX9KbWan5V0sUbJfmcS8MRNHgYuA==", - "dev": true, - "optional": true - }, - "@next/swc-darwin-x64": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.3.tgz", - "integrity": "sha512-ZSWmkg/PxccHFNUSeBdrfaH8KwSkoeUtewXKvuYYt7Ph0yRsbqSyNIvhUezDua96lApiXXq6EL2d1THfeWomvw==", - "dev": true, - "optional": true - }, - "@next/swc-linux-x64-gnu": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.3.tgz", - "integrity": "sha512-PrTBN0iZudAuj4jSbtXcdBdmfpaDCPIneG4Oms4zcs93KwMgLhivYW082Mvlgx9QVEiRm7+RkFpIVtG/i7JitA==", - "dev": true, - "optional": true - }, - "@next/swc-win32-x64-msvc": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.3.tgz", - "integrity": "sha512-mRwbscVjRoHk+tDY7XbkT5d9FCwujFIQJpGp0XNb1i5OHCSDO8WW/C9cLEWS4LxKRbIZlTLYg1MTXqLQkvva8w==", - "dev": true, - "optional": true - }, - "@node-rs/helper": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.2.1.tgz", - "integrity": "sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg==", - "dev": true, - "requires": { - "@napi-rs/triples": "^1.0.3" - } - }, - "@openapi-contrib/openapi-schema-to-json-schema": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz", - "integrity": "sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "@panva/asn1.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", - "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" - }, - "@sideway/address": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", - "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", - "dev": true - }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true - }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "@sinonjs/samsam": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz", - "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, - "@types/accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/aws-lambda": { - "version": "8.10.77", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.77.tgz", - "integrity": "sha512-n0EMFJU/7u3KvHrR83l/zrKOVURXl5pUJPNED/Bzjah89QKCHwCiKCBoVUXRwTGRfCYGIDdinJaAlKDHZdp/Ng==", - "dev": true - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/co-body": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-5.1.1.tgz", - "integrity": "sha512-0/6AjTfQc5OJUchOS4OHiXNPZVuk+5XvEC2vdcizw/bwx0yb0xY7TKSf8JYvQYZ/OJDiAEjWzxnMjGPnSVlPmA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==", - "dev": true - }, - "@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==", - "dev": true - }, - "@types/cookies": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", - "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/express": "*", - "@types/keygrip": "*", - "@types/node": "*" - } - }, - "@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true - }, - "@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", - "dev": true, - "requires": { - "@types/ms": "*" - } - }, - "@types/express": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", - "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" - } - }, - "@types/express-jwt": { - "version": "0.0.42", - "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", - "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", - "requires": { - "@types/express": "*", - "@types/express-unless": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/express-unless": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz", - "integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==", - "requires": { - "@types/express": "*" - } - }, - "@types/hapi__catbox": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/@types/hapi__catbox/-/hapi__catbox-10.2.4.tgz", - "integrity": "sha512-A6ivRrXD5glmnJna1UAGw87QNZRp/vdFO9U4GS+WhOMWzHnw+oTGkMvg0g6y1930CbeheGOCm7A1qHsqH7AXqg==", - "dev": true - }, - "@types/hapi__hapi": { - "version": "20.0.8", - "resolved": "https://registry.npmjs.org/@types/hapi__hapi/-/hapi__hapi-20.0.8.tgz", - "integrity": "sha512-NNslrYq2XQwm4uOqNcSWKpYtaeMr4DkQdrFzSB7p9rKB9ppJLh3mgP2wak9vBZl7/Cnhhb+JVBcUZCOUcW0JPA==", - "dev": true, - "requires": { - "@hapi/boom": "^9.0.0", - "@hapi/iron": "^6.0.0", - "@hapi/podium": "^4.1.3", - "@types/hapi__catbox": "*", - "@types/hapi__mimos": "*", - "@types/hapi__shot": "*", - "@types/node": "*", - "joi": "^17.3.0" - } - }, - "@types/hapi__mimos": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/hapi__mimos/-/hapi__mimos-4.1.4.tgz", - "integrity": "sha512-i9hvJpFYTT/qzB5xKWvDYaSXrIiNqi4ephi+5Lo6+DoQdwqPXQgmVVOZR+s3MBiHoFqsCZCX9TmVWG3HczmTEQ==", - "dev": true, - "requires": { - "@types/mime-db": "*" - } - }, - "@types/hapi__shot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/hapi__shot/-/hapi__shot-4.1.2.tgz", - "integrity": "sha512-8wWgLVP1TeGqgzZtCdt+F+k15DWQvLG1Yv6ZzPfb3D5WIo5/S+GGKtJBVo2uNEcqabP5Ifc71QnJTDnTmw1axA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/http-assert": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", - "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==", - "dev": true - }, - "@types/http-errors": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", - "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "@types/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-mM4TkDpA9oixqg1Fv2vVpOFyIVLJjm5x4k0V+K/rEsizfjD7Tk7LKk3GTtbB7KCfP0FEHQtsZqFxYA0+sijNVg==", - "requires": { - "@types/node": "*" - } - }, - "@types/keygrip": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", - "dev": true - }, - "@types/koa": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.4.tgz", - "integrity": "sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==", - "dev": true, - "requires": { - "@types/accepts": "*", - "@types/content-disposition": "*", - "@types/cookies": "*", - "@types/http-assert": "*", - "@types/http-errors": "*", - "@types/keygrip": "*", - "@types/koa-compose": "*", - "@types/node": "*" - } - }, - "@types/koa-bodyparser": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.3.5.tgz", - "integrity": "sha512-NRqqoTtt7cfdDk/KNo+EwCIKRuzPAu/wsaZ7tgIvSIBtNfxuZHYueaLoWdxX3ZftWavQv07NE46TcpyoZGqpgQ==", - "dev": true, - "requires": { - "@types/koa": "*" - } - }, - "@types/koa-compose": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", - "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", - "dev": true, - "requires": { - "@types/koa": "*" - } - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" - }, - "@types/mime-db": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.1.tgz", - "integrity": "sha512-kGZJY+R+WnR5Rk+RPHUMERtb2qBRViIHCBdtUrY+NmwuGb8pQdfTqQiCKPrxpdoycl8KWm2DLdkpoSdt479XoQ==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, - "@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", - "dev": true - }, - "@types/node": { - "version": "17.0.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", - "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==" - }, - "@types/nodemailer": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", - "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/on-finished": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@types/on-finished/-/on-finished-2.3.1.tgz", - "integrity": "sha512-mzVYaYcFs5Jd2n/O6uYIRUsFRR1cHyZLRvkLCU0E7+G5WhY0qBDAR5fUCeZbvecYOSh9ikhlesyi2UfI8B9ckQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/psl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/psl/-/psl-1.1.0.tgz", - "integrity": "sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==", - "dev": true - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/serve-static": { - "version": "1.13.9", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", - "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/type-is": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", - "integrity": "sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-i1aY7RKb6HmQIEnK0cBmUZUp1URx0riIHw/GYNoZ46Su0GWfLiDmMI8zMRmaauMnOTg2bQag0qfwcyUFC9Tn+A==", - "dev": true - }, - "abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "dev": true - }, - "accept-language": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", - "integrity": "sha1-9QJfF79lpGaoRYOMz5jNuHfYM4Q=", - "dev": true, - "requires": { - "bcp47": "^1.1.2", - "stable": "^0.1.6" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "requires": {} - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "anser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.9.tgz", - "integrity": "sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA==", - "dev": true - }, - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "app-root-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", - "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", - "dev": true - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "assert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", - "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", - "dev": true, - "requires": { - "es6-object-assign": "^1.1.0", - "is-nan": "^1.2.1", - "object-is": "^1.0.1", - "util": "^0.12.0" - } - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "ast-types": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.2.tgz", - "integrity": "sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==", - "dev": true - }, - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "dev": true - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true - }, - "avvio": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.2.tgz", - "integrity": "sha512-XW2CMCmZaCmCCsIaJaLKxAzPwF37fXi1KGxNOvedOpeisLdmxZnblGc3hpHWYnlP+KOUxZsazh43WXNHgXpbqw==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "debug": "^4.0.0", - "fastq": "^1.6.1", - "queue-microtask": "^1.1.2" - } - }, - "aws-sdk": { - "version": "2.1077.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1077.0.tgz", - "integrity": "sha512-orJvJROs8hJaQRfHsX7Zl5PxEgrD/uTXyqXz9Yu9Io5VVxzvnOty9oHmvEMSlgTIf1qd01gnev/vpvP1HgzKtw==", - "dev": true, - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - } - } - }, - "aws-sdk-mock": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.6.2.tgz", - "integrity": "sha512-GRJg8kjRJFLm2aLiPkYSqe/RreHqlqncAeFtWdAbtSxzBdct9EaV6rqSqjyWXKNJG45Rzn2Ojo3F6qVIgkQnSg==", - "dev": true, - "requires": { - "aws-sdk": "^2.928.0", - "sinon": "^11.1.1", - "traverse": "^0.6.6" - }, - "dependencies": { - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "sinon": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", - "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^7.1.2", - "@sinonjs/samsam": "^6.0.2", - "diff": "^5.0.0", - "nise": "^5.1.0", - "supports-color": "^7.2.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, - "bcp47": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", - "integrity": "sha1-NUvjMH/9CEM6ePXh4glYRfifx/4=", - "dev": true - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "bl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", - "dev": true, - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", - "dev": true - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - } - }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - }, - "cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "requires": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "requires": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "capital-case": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", - "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "change-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", - "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, - "requires": { - "camel-case": "^4.1.2", - "capital-case": "^1.0.4", - "constant-case": "^3.0.4", - "dot-case": "^3.0.4", - "header-case": "^2.0.4", - "no-case": "^3.0.4", - "param-case": "^3.0.4", - "pascal-case": "^3.1.2", - "path-case": "^3.0.4", - "sentence-case": "^3.0.4", - "snake-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", - "dev": true - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "dev": true - }, - "cldrjs": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.5.tgz", - "integrity": "sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==", - "dev": true - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "co-body": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.1.0.tgz", - "integrity": "sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==", - "requires": { - "inflation": "^2.0.0", - "qs": "^6.5.2", - "raw-body": "^2.3.3", - "type-is": "^1.6.16" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "constant-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", - "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case": "^2.0.2" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "requires": { - "safe-buffer": "5.2.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dev": true, - "requires": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true - } - } - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "cookiejar": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", - "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", - "dev": true - }, - "cookies": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", - "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", - "dev": true, - "requires": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - } - } - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", - "dev": true - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", - "dev": true - }, - "cssnano-preset-simple": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssnano-preset-simple/-/cssnano-preset-simple-3.0.0.tgz", - "integrity": "sha512-vxQPeoMRqUT3c/9f0vWeVa2nKQIHFpogtoBvFdW4GQ3IvEJ6uauCP6p3Y5zQDLFcI7/+40FTgX12o7XUL0Ko+w==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001202" - } - }, - "cssnano-simple": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssnano-simple/-/cssnano-simple-3.0.0.tgz", - "integrity": "sha512-oU3ueli5Dtwgh0DyeohcIEE00QVfbPR3HzyXdAl89SfnQG3y0/qcpfLVW+jPIh3/rgMZGwuW96rejZGaYE9eUg==", - "dev": true, - "requires": { - "cssnano-preset-simple": "^3.0.0" - } - }, - "data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "dev": true - }, - "dayjs": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.0.tgz", - "integrity": "sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "domain-browser": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.19.0.tgz", - "integrity": "sha512-fRA+BaAWOR/yr/t7T9E9GJztHPeFjj8U35ajyAjCDtAAnTn1Rc1f6W6VGPJrO1tkQv9zWu+JRof7z6oQtiYVFQ==", - "dev": true - }, - "dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "dev": true - }, - "dotenv-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dotenv-json/-/dotenv-json-1.0.0.tgz", - "integrity": "sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ==", - "dev": true - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "ejs": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", - "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", - "dev": true, - "requires": { - "jake": "^10.8.5" - } - }, - "electron-to-chromium": { - "version": "1.4.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", - "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", - "dev": true - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - } - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dev": true, - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-json-stringify": { - "version": "2.7.13", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz", - "integrity": "sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==", - "dev": true, - "requires": { - "ajv": "^6.11.0", - "deepmerge": "^4.2.2", - "rfdc": "^1.2.0", - "string-similarity": "^4.0.1" - } - }, - "fast-redact": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.0.tgz", - "integrity": "sha512-dir8LOnvialLxiXDPESMDHGp82CHi6ZEYTVkcvdn5d7psdv9ZkkButXrOeXST4aqreIRR+N7CYlsrwFuorurVg==", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true - }, - "fastify": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.18.1.tgz", - "integrity": "sha512-OA0imy/bQCMzf7LUCb/1JI3ZSoA0Jo0MLpYULxV7gpppOpJ8NBxDp2PQoQ0FDqJevZPb7tlZf5JacIQft8x9yw==", - "dev": true, - "requires": { - "@fastify/ajv-compiler": "^1.0.0", - "abstract-logging": "^2.0.0", - "avvio": "^7.1.2", - "fast-json-stringify": "^2.5.2", - "fastify-error": "^0.3.0", - "fastify-warning": "^0.2.0", - "find-my-way": "^4.0.0", - "flatstr": "^1.0.12", - "light-my-request": "^4.2.0", - "pino": "^6.2.1", - "proxy-addr": "^2.0.7", - "readable-stream": "^3.4.0", - "rfdc": "^1.1.4", - "secure-json-parse": "^2.0.0", - "semver": "^7.3.2", - "tiny-lru": "^7.0.0" - } - }, - "fastify-error": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", - "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==", - "dev": true - }, - "fastify-warning": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", - "integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==", - "dev": true - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "requires": { - "minimatch": "^5.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", - "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - } - } - }, - "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-my-way": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz", - "integrity": "sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg==", - "dev": true, - "requires": { - "fast-decode-uri-component": "^1.0.1", - "fast-deep-equal": "^3.1.3", - "safe-regex2": "^2.0.0", - "semver-store": "^0.3.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz", - "integrity": "sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true - } - } - }, - "flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==", - "dev": true - }, - "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", - "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", - "dev": true - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "peer": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-orientation": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-orientation/-/get-orientation-1.1.2.tgz", - "integrity": "sha512-/pViTfifW+gBbh/RnlFYHINvELT9Znt+SYyDKAUL6uV6By019AK/s+i9XP4jSwq7lwP38Fd8HVeTxym3+hkwmQ==", - "dev": true, - "requires": { - "stream-parser": "^0.3.1" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "globalize": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.7.0.tgz", - "integrity": "sha512-faR46vTIbFCeAemyuc9E6/d7Wrx9k2ae2L60UhakztFg6VuE42gENVJNuPFtt7Sdjrk9m2w8+py7Jj+JTNy59w==", - "dev": true, - "requires": { - "cldrjs": "^0.5.4" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "peer": true - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "header-case": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", - "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, - "requires": { - "capital-case": "^1.0.4", - "tslib": "^2.0.3" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "http-assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", - "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", - "dev": true, - "requires": { - "deep-equal": "~1.0.1", - "http-errors": "~1.8.0" - } - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "dependencies": { - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - } - } - }, - "http-status": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-status/-/http-status-1.5.0.tgz", - "integrity": "sha512-wcGvY31MpFNHIkUcXHHnvrE4IKYlpvitJw5P/1u892gMBAM46muQ+RH7UN1d+Ntnfx5apnOnVY6vcLmrWHOLwg==", - "dev": true - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "hyperid": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-2.3.1.tgz", - "integrity": "sha512-mIbI7Ymn6MCdODaW1/6wdf5lvvXzmPsARN4zTLakMmcziBOuP4PxCBJvHF6kbAIHX6H4vAELx/pDmt0j6Th5RQ==", - "dev": true, - "requires": { - "uuid": "^8.3.2", - "uuid-parse": "^1.1.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "image-size": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz", - "integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==", - "dev": true, - "requires": { - "queue": "6.0.2" - } - }, - "inflation": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", - "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" - }, - "inflection": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.2.tgz", - "integrity": "sha512-cmZlljCRTBFouT8UzMzrGcVEvkv6D/wBdcdKG7J1QH5cXjtU75Dm+P27v9EKu/Y43UYyCJd1WC4zLebRrC8NBw==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "invert-kv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", - "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz", - "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0" - } - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", - "dev": true, - "requires": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-worker": { - "version": "27.0.0-next.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.0-next.5.tgz", - "integrity": "sha512-mk0umAQ5lT+CaOJ+Qp01N6kz48sJG2kr2n1rX0koqKf6FIygQV0qLOdN9SCYID4IVeSigDOcPeGLozdMLYfb5g==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "dev": true - }, - "joi": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", - "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - } - }, - "jose": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", - "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", - "requires": { - "@panva/asn1.js": "^1.0.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "dev": true, - "requires": { - "xmlcreate": "^2.0.4" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "peer": true - }, - "json-merge-patch": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-merge-patch/-/json-merge-patch-1.0.2.tgz", - "integrity": "sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "json-schema-compare": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", - "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", - "dev": true, - "requires": { - "lodash": "^4.17.4" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "peer": true, - "requires": { - "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "peer": true - } - } - }, - "jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", - "dev": true - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jwks-rsa": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", - "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", - "requires": { - "@types/express-jwt": "0.0.42", - "debug": "^4.3.2", - "jose": "^2.0.5", - "limiter": "^1.1.5", - "lru-memoizer": "^2.1.4" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "dev": true, - "requires": { - "tsscmp": "1.0.6" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "koa": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.4.tgz", - "integrity": "sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==", - "dev": true, - "requires": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.8.0", - "debug": "^4.3.2", - "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - } - } - }, - "koa-compose": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", - "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", - "dev": true - }, - "koa-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", - "dev": true, - "requires": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - } - }, - "lambda-event-mock": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lambda-event-mock/-/lambda-event-mock-1.5.0.tgz", - "integrity": "sha512-vx1d+vZqi7FF6B3+mAfHnY/6Tlp6BheL2ta0MJS0cIRB3Rc4I5cviHTkiJxHdE156gXx3ZjlQRJrS4puXvtrhA==", - "dev": true, - "requires": { - "@extra-number/significant-digits": "^1.1.1", - "clone-deep": "^4.0.1", - "uuid": "^3.3.3", - "vandium-utils": "^1.2.0" - }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "vandium-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vandium-utils/-/vandium-utils-1.2.0.tgz", - "integrity": "sha1-RHNd5LdkGgXeWevpRfF05YLbT1k=", - "dev": true - } - } - }, - "lambda-leak": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lambda-leak/-/lambda-leak-2.0.0.tgz", - "integrity": "sha1-dxmF02KEh/boha+uK1RRDc+yzX4=", - "dev": true - }, - "lambda-tester": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lambda-tester/-/lambda-tester-4.0.1.tgz", - "integrity": "sha512-ft6XHk84B6/dYEzyI3anKoGWz08xQ5allEHiFYDUzaYTymgVK7tiBkCEbuWx+MFvH7OpFNsJXVtjXm0X8iH3Iw==", - "dev": true, - "requires": { - "app-root-path": "^3.0.0", - "dotenv": "^8.0.0", - "dotenv-json": "^1.0.0", - "lambda-event-mock": "^1.5.0", - "lambda-leak": "^2.0.0", - "semver": "^6.1.1", - "uuid": "^3.3.3", - "vandium-utils": "^2.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } - } - }, - "lcid": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", - "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", - "dev": true, - "requires": { - "invert-kv": "^3.0.0" - } - }, - "libphonenumber-js": { - "version": "1.9.49", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.49.tgz", - "integrity": "sha512-/wEOIONcVboFky+lWlCaF7glm1FhBz11M5PHeCApA+xDdVfmhKjHktHS8KjyGxouV5CSXIr4f3GvLSpJa4qMSg==" - }, - "light-my-request": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.8.0.tgz", - "integrity": "sha512-C2XESrTRsZnI59NSQigOsS6IuTxpj8OhSBvZS9fhgBMsamBsAuWN1s4hj/nCi8EeZcyAA6xbROhsZy7wKdfckg==", - "dev": true, - "requires": { - "ajv": "^8.1.0", - "cookie": "^0.4.0", - "process-warning": "^1.0.0", - "set-cookie-parser": "^2.4.1" - }, - "dependencies": { - "ajv": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz", - "integrity": "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, - "limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true - } - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "loopback-connector": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-5.0.1.tgz", - "integrity": "sha512-aSPT6x5WZdoW9ylyNE4CxGqFbIqC9cSEZJwWkCincso27PXlZPj52POoF6pgxug9mkH7MrbXuP3SSDLkLq5oQQ==", - "dev": true, - "requires": { - "async": "^3.2.0", - "bluebird": "^3.7.2", - "debug": "^4.1.1", - "msgpack5": "^4.2.0", - "strong-globalize": "^6.0.4", - "uuid": "^8.3.0" - } - }, - "loopback-datasource-juggler": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-4.26.0.tgz", - "integrity": "sha512-/R40jUGDrnRBgTh121L4Y7sHDF0KxbgSAN4gLJKp8xGNQ6KpkSQyqZkmap98eN7B75RES78DS3MGghsYMvAJ3Q==", - "dev": true, - "requires": { - "async": "^3.1.0", - "change-case": "^4.1.1", - "debug": "^4.1.0", - "depd": "^2.0.0", - "inflection": "^1.6.0", - "lodash": "^4.17.11", - "loopback-connector": "^5.0.0", - "minimatch": "^3.0.3", - "qs": "^6.5.0", - "shortid": "^2.2.6", - "strong-globalize": "^6.0.5", - "traverse": "^0.6.6", - "uuid": "^8.3.1" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - } - } - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "dev": true, - "requires": { - "get-func-name": "^2.0.0" - } - }, - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "lru-cache": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", - "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", - "requires": { - "pseudomap": "^1.0.1", - "yallist": "^2.0.0" - } - }, - "lru-memoizer": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", - "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", - "requires": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "~4.0.0" - } - }, - "lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "marked": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.12.tgz", - "integrity": "sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==", - "dev": true - }, - "md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "mem": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", - "integrity": "sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^2.1.0", - "p-is-promise": "^2.1.0" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" - }, - "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "requires": { - "mime-db": "1.51.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "msgpack5": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.5.1.tgz", - "integrity": "sha512-zC1vkcliryc4JGlL6OfpHumSYUHWFGimSI+OgfRCjTFLmKA2/foR9rMTOhWiqfOrfxJOctrpWPvrppf8XynJxw==", - "dev": true, - "requires": { - "bl": "^2.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.3.6", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "multimatch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", - "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", - "dev": true, - "requires": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - } - }, - "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true - }, - "native-url": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/native-url/-/native-url-0.3.4.tgz", - "integrity": "sha512-6iM8R99ze45ivyH8vybJ7X0yekIcPf5GgLV5K0ENCbmRcaRIDoj37BC8iLEmaaBfqqb8enuZ5p0uhY+lVAbAcA==", - "dev": true, - "requires": { - "querystring": "^0.2.0" - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - }, - "next": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/next/-/next-11.1.3.tgz", - "integrity": "sha512-ud/gKmnKQ8wtHC+pd1ZiqPRa7DdgulPkAk94MbpsspfNliwZkYs9SIYWhlLSyg+c661LzdUI2nZshvrtggSYWA==", - "dev": true, - "requires": { - "@babel/runtime": "7.15.3", - "@hapi/accept": "5.0.2", - "@next/env": "11.1.3", - "@next/polyfill-module": "11.1.3", - "@next/react-dev-overlay": "11.1.3", - "@next/react-refresh-utils": "11.1.3", - "@next/swc-darwin-arm64": "11.1.3", - "@next/swc-darwin-x64": "11.1.3", - "@next/swc-linux-x64-gnu": "11.1.3", - "@next/swc-win32-x64-msvc": "11.1.3", - "@node-rs/helper": "1.2.1", - "assert": "2.0.0", - "ast-types": "0.13.2", - "browserify-zlib": "0.2.0", - "browserslist": "4.16.6", - "buffer": "5.6.0", - "caniuse-lite": "^1.0.30001228", - "chalk": "2.4.2", - "chokidar": "3.5.1", - "constants-browserify": "1.0.0", - "crypto-browserify": "3.12.0", - "cssnano-simple": "3.0.0", - "domain-browser": "4.19.0", - "encoding": "0.1.13", - "etag": "1.8.1", - "find-cache-dir": "3.3.1", - "get-orientation": "1.1.2", - "https-browserify": "1.0.0", - "image-size": "1.0.0", - "jest-worker": "27.0.0-next.5", - "native-url": "0.3.4", - "node-fetch": "2.6.1", - "node-html-parser": "1.4.9", - "node-libs-browser": "^2.2.1", - "os-browserify": "0.3.0", - "p-limit": "3.1.0", - "path-browserify": "1.0.1", - "pnp-webpack-plugin": "1.6.4", - "postcss": "8.2.15", - "process": "0.11.10", - "querystring-es3": "0.2.1", - "raw-body": "2.4.1", - "react-is": "17.0.2", - "react-refresh": "0.8.3", - "stream-browserify": "3.0.0", - "stream-http": "3.1.1", - "string_decoder": "1.3.0", - "styled-jsx": "4.0.1", - "timers-browserify": "2.0.12", - "tty-browserify": "0.0.1", - "use-subscription": "1.5.1", - "util": "0.12.4", - "vm-browserify": "1.1.2", - "watchpack": "2.1.1" - }, - "dependencies": { - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "raw-body": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", - "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.3", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - } - } - }, - "next-test-api-route-handler": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/next-test-api-route-handler/-/next-test-api-route-handler-3.1.8.tgz", - "integrity": "sha512-8o+TjtlQdvLv7YmR5NOl+AQCM/tEZqB1mvqBeGFQscuGtL+42fOrOsDFv2QsFUWieUMKv7j7NqOmYMpJRPu3/w==", - "dev": true, - "requires": { - "cookie": "^0.5.0", - "node-fetch": "^2.6.7" - }, - "dependencies": { - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true - }, - "node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - } - } - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "nise": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz", - "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": ">=5", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "requires": { - "isarray": "0.0.1" - } - } - } - }, - "no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "requires": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "nock": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/nock/-/nock-11.7.0.tgz", - "integrity": "sha512-7c1jhHew74C33OBeRYyQENT+YXQiejpwIrEjinh6dRurBae+Ei4QjeUaPlkptIF0ZacEiVCnw8dWaxqepkiihg==", - "dev": true, - "requires": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.13", - "mkdirp": "^0.5.0", - "propagate": "^2.0.0" - } - }, - "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - }, - "node-html-parser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.4.9.tgz", - "integrity": "sha512-UVcirFD1Bn0O+TSmloHeHqZZCxHjvtIeGdVdGMhyZ8/PWlEiZaZ5iJzR189yKZr8p0FXN58BUeC7RHRkf/KYGw==", - "dev": true, - "requires": { - "he": "1.2.0" - } - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - } - } - }, - "node-releases": { - "version": "1.1.77", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz", - "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==", - "dev": true - }, - "nodemailer": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.3.tgz", - "integrity": "sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "object-inspect": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", - "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==" - }, - "object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", - "integrity": "sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=", - "dev": true - }, - "openapi3-ts": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", - "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", - "dev": true, - "requires": { - "yaml": "^1.10.2" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-locale": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz", - "integrity": "sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==", - "dev": true, - "requires": { - "execa": "^4.0.0", - "lcid": "^3.0.0", - "mem": "^5.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dev": true, - "requires": { - "p-timeout": "^3.1.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } - } - }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "path-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", - "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-to-regexp": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", - "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true, - "peer": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", - "dev": true, - "requires": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" - } - }, - "pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "platform": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", - "dev": true - }, - "pnp-webpack-plugin": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", - "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", - "dev": true, - "requires": { - "ts-pnp": "^1.1.6" - } - }, - "postcss": { - "version": "8.2.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.15.tgz", - "integrity": "sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==", - "dev": true, - "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", - "dev": true - }, - "pretty-quick": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.1.3.tgz", - "integrity": "sha512-kOCi2FJabvuh1as9enxYmrnBC6tVMoVOenMaBqRfsvBHB0cbpYHjdQEpSglpASDFEXVwplpcGR4CLEaisYAFcA==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "execa": "^4.0.0", - "find-up": "^4.1.0", - "ignore": "^5.1.4", - "mri": "^1.1.5", - "multimatch": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "dev": true - }, - "propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "dev": true, - "requires": { - "inherits": "~2.0.3" - } - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - } - } - }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dev": true, - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "react-refresh": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", - "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", - "dev": true, - "requires": { - "ret": "~0.2.0" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", - "dev": true - }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, - "peer": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, - "secure-json-parse": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", - "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==", - "dev": true - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "semver-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", - "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==", - "dev": true - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - } - } - }, - "sentence-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", - "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-cookie-parser": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz", - "integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg==", - "dev": true - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "shell-quote": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", - "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", - "dev": true - }, - "shiki": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz", - "integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==", - "dev": true, - "requires": { - "jsonc-parser": "^3.0.0", - "vscode-oniguruma": "^1.6.1", - "vscode-textmate": "5.2.0" - } - }, - "shortid": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", - "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", - "dev": true, - "requires": { - "nanoid": "^2.1.0" - }, - "dependencies": { - "nanoid": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", - "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==", - "dev": true - } - } - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "sinon": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.0.tgz", - "integrity": "sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.1.2", - "@sinonjs/samsam": "^6.1.1", - "diff": "^5.0.0", - "nise": "^5.1.1", - "supports-color": "^7.2.0" - }, - "dependencies": { - "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", - "dev": true, - "requires": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } - }, - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "requires": { - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dev": true, - "requires": { - "type-fest": "^0.7.1" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true - }, - "stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dev": true, - "requires": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "stream-http": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.1.1.tgz", - "integrity": "sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - } - }, - "stream-parser": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", - "integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=", - "dev": true, - "requires": { - "debug": "2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", - "dev": true - }, - "string-similarity": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", - "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "strong-error-handler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-4.0.0.tgz", - "integrity": "sha512-Ki59WSOfSEod6IkDUB4uf9+DwkCLQRbEdYqen167I/zyPps9x9gS+UzhLZOcer58RA6iFmoGg/+CN/x5d+Cv3Q==", - "dev": true, - "requires": { - "@types/express": "^4.16.0", - "accepts": "^1.3.3", - "debug": "^4.1.1", - "ejs": "^3.1.3", - "fast-safe-stringify": "^2.0.6", - "http-status": "^1.1.2", - "js2xmlparser": "^4.0.0", - "strong-globalize": "^6.0.1" - } - }, - "strong-globalize": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-6.0.5.tgz", - "integrity": "sha512-7nfUli41TieV9/TSc0N62ve5Q4nfrpy/T0nNNy6TyD3vst79QWmeylCyd3q1gDxh8dqGEtabLNCdPQP1Iuvecw==", - "dev": true, - "requires": { - "accept-language": "^3.0.18", - "debug": "^4.2.0", - "globalize": "^1.6.0", - "lodash": "^4.17.20", - "md5": "^2.3.0", - "mkdirp": "^1.0.4", - "os-locale": "^5.0.0", - "yamljs": "^0.3.0" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } - } - }, - "styled-jsx": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-4.0.1.tgz", - "integrity": "sha512-Gcb49/dRB1k8B4hdK8vhW27Rlb2zujCk1fISrizCcToIs+55B4vmUM0N9Gi4nnVfFZWe55jRdWpAqH1ldAKWvQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "7.14.5", - "@babel/types": "7.15.0", - "convert-source-map": "1.7.0", - "loader-utils": "1.2.3", - "source-map": "0.7.3", - "string-hash": "1.1.3", - "stylis": "3.5.4", - "stylis-rule-sheet": "0.0.10" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "stylis": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", - "dev": true - }, - "stylis-rule-sheet": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", - "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", - "dev": true, - "requires": {} - }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "supertest": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-4.0.2.tgz", - "integrity": "sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^3.8.3" - } - }, - "supertokens-js-override": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/supertokens-js-override/-/supertokens-js-override-0.0.4.tgz", - "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==" - }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "tiny-lru": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", - "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "toposort": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=", - "dev": true - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", - "dev": true - }, - "ts-pnp": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", - "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", - "dev": true - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, - "tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "dev": true - }, - "tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", - "dev": true - }, - "twilio": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.7.2.tgz", - "integrity": "sha512-sTdEwAhkDzoDoXE3i83F/CdZegZ5O1FPDY0hAnJmGr/TjDqGl+Q6WzjC0+9cTmQnjCaDg6H4L97UZeJLFFEh3A==", - "requires": { - "axios": "^0.26.1", - "dayjs": "^1.8.29", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^9.0.0", - "qs": "^6.9.4", - "scmp": "^2.1.0", - "url-parse": "^1.5.9", - "xmlbuilder": "^13.0.2" - }, - "dependencies": { - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "requires": { - "follow-redirects": "^1.14.8" - } - }, - "xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==" - } - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedoc": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", - "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", - "dev": true, - "requires": { - "glob": "^7.2.0", - "lunr": "^2.3.9", - "marked": "^4.0.10", - "minimatch": "^3.0.4", - "shiki": "^0.10.0" - }, - "dependencies": { - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true - }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", - "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "upper-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", - "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "use-subscription": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.5.1.tgz", - "integrity": "sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1" - } - }, - "util": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", - "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "safe-buffer": "^5.1.2", - "which-typed-array": "^1.1.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - }, - "uuid-parse": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", - "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", - "dev": true - }, - "validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", - "dev": true - }, - "vandium-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vandium-utils/-/vandium-utils-2.0.0.tgz", - "integrity": "sha512-XWbQ/0H03TpYDXk8sLScBEZpE7TbA0CHDL6/Xjt37IBYKLsHUQuBlL44ttAUs9zoBOLFxsW7HehXcuWCNyqOxQ==", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "verify-apple-id-token": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/verify-apple-id-token/-/verify-apple-id-token-3.0.1.tgz", - "integrity": "sha512-q91pG1e52TpEzXldMirWYNWcSQC4WuzgG0y/ZnBhzjfk0pSxi4YlGh5OTVRlodBenayGHfSDn5VseG9QDuqOew==", - "requires": { - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.0.0" - }, - "dependencies": { - "@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "jose": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.4.tgz", - "integrity": "sha512-94FdcR8felat4vaTJyL/WVdtlWLlsnLMZP8v+A0Vru18K3bQ22vn7TtpVh3JlgBFNIlYOUlGqwp/MjRPOnIyCQ==" - }, - "jwks-rsa": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", - "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", - "requires": { - "@types/express": "^4.17.14", - "@types/jsonwebtoken": "^9.0.0", - "debug": "^4.3.4", - "jose": "^4.10.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.1.4" - } - } - } - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "vscode-oniguruma": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz", - "integrity": "sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ==", - "dev": true - }, - "vscode-textmate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", - "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==", - "dev": true - }, - "watchpack": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz", - "integrity": "sha512-Oo7LXCmc1eE1AjyuSBmtC3+Wy4HcV8PxWh2kP6fOl8yTlNS7r0K9l1ao2lrrUza7V39Y3D/BbJgY8VeSlc5JKw==", - "dev": true, - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "which-typed-array": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", - "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.7" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "dev": true, - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true - }, - "xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yamljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", - "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - } - } - }, - "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "ylru": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", - "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } - } -} diff --git a/package.json b/package.json index 13060930f..e7ceaf69d 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,271 @@ "name": "supertokens-node", "version": "13.6.0", "description": "NodeJS driver for SuperTokens core", - "main": "index.js", + "engines": { + "node": ">=14" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./utils": { + "types": "./dist/utils/index.d.ts", + "import": "./dist/utils/index.mjs", + "require": "./dist/utils/index.js" + }, + "./processState": { + "types": "./dist/processState.d.ts", + "import": "./dist/processState.mjs", + "require": "./dist/processState.js" + }, + "./framework/*": { + "types": "./dist/framework/*/index.d.ts", + "import": "./dist/framework/*/index.mjs", + "require": "./dist/framework/*/index.js" + }, + "./framework": { + "types": "./dist/framework/index.d.ts", + "import": "./dist/framework/index.mjs", + "require": "./dist/framework/index.js" + }, + "./recipe/dashboard": { + "types": "./dist/recipe/dashboard/index.d.ts", + "import": "./dist/recipe/dashboard/index.mjs", + "require": "./dist/recipe/dashboard/index.js" + }, + "./recipe/emailpassword": { + "types": "./dist/recipe/emailpassword/index.d.ts", + "import": "./dist/recipe/emailpassword/index.mjs", + "require": "./dist/recipe/emailpassword/index.js" + }, + "./recipe/emailpassword/emaildelivery": { + "types": "./dist/recipe/emailpassword/emaildelivery/index.d.ts", + "import": "./dist/recipe/emailpassword/emaildelivery/index.mjs", + "require": "./dist/recipe/emailpassword/emaildelivery/index.js" + }, + "./recipe/emailverification": { + "types": "./dist/recipe/emailverification/index.d.ts", + "import": "./dist/recipe/emailverification/index.mjs", + "require": "./dist/recipe/emailverification/index.js" + }, + "./recipe/emailverification/emaildelivery": { + "types": "./dist/recipe/emailverification/emaildelivery/index.d.ts", + "import": "./dist/recipe/emailverification/emaildelivery/index.mjs", + "require": "./dist/recipe/emailverification/emaildelivery/index.js" + }, + "./recipe/jwt": { + "types": "./dist/recipe/jwt/index.d.ts", + "import": "./dist/recipe/jwt/index.mjs", + "require": "./dist/recipe/jwt/index.js" + }, + "./recipe/openid": { + "types": "./dist/recipe/openid/index.d.ts", + "import": "./dist/recipe/openid/index.mjs", + "require": "./dist/recipe/openid/index.js" + }, + "./recipe/passwordless": { + "types": "./dist/recipe/passwordless/index.d.ts", + "import": "./dist/recipe/passwordless/index.mjs", + "require": "./dist/recipe/passwordless/index.js" + }, + "./recipe/passwordless/emaildelivery": { + "types": "./dist/recipe/passwordless/emaildelivery/index.d.ts", + "import": "./dist/recipe/passwordless/emaildelivery/index.mjs", + "require": "./dist/recipe/passwordless/emaildelivery/index.js" + }, + "./recipe/passwordless/smsdelivery": { + "types": "./dist/recipe/passwordless/smsdelivery/index.d.ts", + "import": "./dist/recipe/passwordless/smsdelivery/index.mjs", + "require": "./dist/recipe/passwordless/smsdelivery/index.js" + }, + "./recipe/session": { + "types": "./dist/recipe/session/index.d.ts", + "import": "./dist/recipe/session/index.mjs", + "require": "./dist/recipe/session/index.js" + }, + "./recipe/session/claims": { + "types": "./dist/recipe/session/claims.d.ts", + "import": "./dist/recipe/session/claims.mjs", + "require": "./dist/recipe/session/claims.js" + }, + "./recipe/session/framework": { + "types": "./dist/recipe/index.d.ts", + "import": "./dist/recipe/index.mjs", + "require": "./dist/recipe/index.js" + }, + "./recipe/session/framework/*": { + "types": "./dist/session/framework/*.d.ts", + "import": "./dist/session/framework/*.mjs", + "require": "./dist/session/framework/*.js" + }, + "./recipe/thirdparty": { + "types": "./dist/recipe/thirdparty/index.d.ts", + "import": "./dist/recipe/thirdparty/index.mjs", + "require": "./dist/recipe/thirdparty/index.js" + }, + "./recipe/thirdparty/providers/*": { + "types": "./dist/recipe/thirdparty/providers/*.d.ts", + "import": "./dist/recipe/thirdparty/providers/*.mjs", + "require": "./dist/recipe/thirdparty/providers/*.js" + }, + "./recipe/thirdparty/providers": { + "types": "./dist/recipe/thirdparty/providers/index.d.ts", + "import": "./dist/recipe/thirdparty/providers/index.mjs", + "require": "./dist/recipe/thirdparty/providers/index.js" + }, + "./recipe/thirdpartyemailpassword": { + "types": "./dist/recipe/thirdpartyemailpassword/index.d.ts", + "import": "./dist/recipe/thirdpartyemailpassword/index.mjs", + "require": "./dist/recipe/thirdpartyemailpassword/index.js" + }, + "./recipe/thirdpartyemailpassword/emaildelivery": { + "types": "./dist/recipe/thirdpartyemailpassword/emaildelivery/index.d.ts", + "import": "./dist/recipe/thirdpartyemailpassword/emaildelivery/index.mjs", + "require": "./dist/recipe/thirdpartyemailpassword/emaildelivery/index.js" + }, + "./recipe/thirdpartypasswordless": { + "types": "./dist/recipe/thirdpartypasswordless/index.d.ts", + "import": "./dist/recipe/thirdpartypasswordless/index.mjs", + "require": "./dist/recipe/thirdpartypasswordless/index.js" + }, + "./recipe/thirdpartypasswordless/emaildelivery": { + "types": "./dist/recipe/thirdpartypasswordless/emaildelivery/index.d.ts", + "import": "./dist/recipe/thirdpartypasswordless/emaildelivery/index.mjs", + "require": "./dist/recipe/thirdpartypasswordless/emaildelivery/index.js" + }, + "./recipe/thirdpartypasswordless/smsdelivery": { + "types": "./dist/recipe/thirdpartypasswordless/smsdelivery/index.d.ts", + "import": "./dist/recipe/thirdpartypasswordless/smsdelivery/index.mjs", + "require": "./dist/recipe/thirdpartypasswordless/smsdelivery/index.js" + }, + "./recipe/usermetadata": { + "types": "./dist/recipe/usermetadata/index.d.ts", + "import": "./dist/recipe/usermetadata/index.mjs", + "require": "./dist/recipe/usermetadata/index.js" + }, + "./recipe/userroles": { + "types": "./dist/recipe/userroles/index.d.ts", + "import": "./dist/recipe/userroles/index.mjs", + "require": "./dist/recipe/userroles/index.js" + } + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "utils": [ + "./dist/utils.d.ts" + ], + "processState": [ + "./dist/processState.d.ts" + ], + "framework": [ + "./dist/framework/index.d.ts" + ], + "framework/*": [ + "./dist/framework/*/index.d.ts" + ], + "recipe/*": [ + "./dist/recipe/*/index.d.ts" + ], + "nextjs": [ + "./dist/nextjs.d.ts" + ], + "types": [ + "./dist/types.d.ts" + ], + "recipe/dashboard": [ + "./dist/recipe/dashboard/index.d.ts" + ], + "recipe/emailpassword": [ + "./dist/recipe/emailpassword/index.d.ts" + ], + "recipe/emailpassword/emaildelivery": [ + "./dist/recipe/emailpassword/emaildelivery/index.d.ts" + ], + "recipe/emailverification": [ + "./dist/recipe/emailverification/index.d.ts" + ], + "recipe/jwt": [ + "./dist/recipe/jwt/index.d.ts" + ], + "recipe/openid": [ + "./dist/recipe/openid/index.d.ts" + ], + "recipe/passwordless": [ + "./dist/recipe/passwordless/index.d.ts" + ], + "recipe/passwordless/emaildelivery": [ + "./dist/recipe/passwordless/emaildelivery/index.d.ts" + ], + "recipe/passwordless/smsdelivery": [ + "./dist/recipe/passwordless/smsdelivery/index.d.ts" + ], + "recipe/session/framework": [ + "./dist/recipe/index.d.ts" + ], + "recipe/session/framework/*": [ + "./dist/recipe/*" + ], + "recipe/session": [ + "./dist/recipe/session/index.d.ts" + ], + "recipe/session/claims": [ + "./dist/recipe/session/claims.d.ts" + ], + "recipe/thirdparty": [ + "./dist/recipe/thirdparty/index.d.ts" + ], + "recipe/thirdparty/providers/*": [ + "./dist/recipe/thirdparty/providers/*/index.d.ts" + ], + "recipe/thirdpartyemailpassword": [ + "./dist/recipe/thirdpartyemailpassword/index.d.ts" + ], + "recipe/thirdpartyemailpassword/emaildelivery": [ + "./dist/recipe/thirdpartyemailpassword/emaildelivery/index.d.ts" + ], + "recipe/thirdpartypasswordless": [ + "./dist/recipe/thirdpartypasswordless/index.d.ts" + ], + "recipe/thirdpartypasswordless/emaildelivery": [ + "./dist/recipe/thirdpartypasswordless/emaildelivery/index.d.ts" + ], + "recipe/thirdpartypasswordless/smsdelivery": [ + "./dist/recipe/thirdpartypasswordless/smsdelivery/index.d.ts" + ], + "recipe/usermetadata": [ + "./dist/recipe/usermetadata/index.d.ts" + ], + "recipe/userroles": [ + "./dist/recipe/userroles/index.d.ts" + ] + } + }, + "packageManager": "pnpm@7.28.0", "scripts": { - "test": "TEST_MODE=testing npx mocha --timeout 500000", + "test": "TEST_MODE=testing INSTALL_PATH=./../supertokens-root vitest", + "test:watch": "vitest --watch", + "dev": "tsup --watch", "build-check": "cd lib && npx tsc -p tsconfig.json --noEmit && cd ../test/with-typescript && npx tsc -p tsconfig.json --noEmit", - "build": "cd lib && rm -rf build && npx tsc -p tsconfig.json && cd ../test/with-typescript && npx tsc -p tsconfig.json --noEmit && cd ../.. && npm run post-build", + "build": "tsup", "pretty": "npx pretty-quick .", "post-build": "node add-ts-no-check.js", "build-pretty": "npm run build && npm run pretty && npm run pretty", "pretty-check": "npx pretty-quick --check .", "set-up-hooks": "cp hooks/pre-commit.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit", - "build-docs": "rm -rf ./docs && npx typedoc --out ./docs --tsconfig ./lib/tsconfig.json ./lib/ts/index.ts ./lib/ts/**/index.ts ./lib/ts/**/*/index.ts" + "build-docs": "rm -rf ./docs && npx typedoc --out ./docs --tsconfig ./tsconfig.json ./src/index.ts ./src/**/index.ts ./src/**/*/index.ts", + "lint": "eslint --ext .ts,.tsx,.js,.jsx .", + "lint:fix": "eslint --ext .ts,.tsx,.js,.jsx . --fix" }, "keywords": [ "auth", @@ -34,58 +288,72 @@ }, "homepage": "https://github.com/supertokens/supertokens-node#readme", "dependencies": { - "axios": "0.21.4", - "body-parser": "1.20.1", + "@hapi/boom": "^10.0.1", + "axios": "^1.3.4", + "body-parser": "^1.20.2", "co-body": "6.1.0", - "cookie": "0.4.0", - "debug": "^4.3.3", + "cookie": "^0.5.0", + "debug": "^4.3.4", "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^2.0.5", - "libphonenumber-js": "^1.9.44", - "nodemailer": "^6.7.2", - "psl": "1.8.0", + "jwks-rsa": "^3.0.1", + "libphonenumber-js": "^1.10.21", + "nodemailer": "^6.9.1", + "psl": "^1.9.0", "supertokens-js-override": "^0.0.4", - "twilio": "^4.7.2", + "twilio": "^4.8.0", "verify-apple-id-token": "^3.0.1" }, "devDependencies": { - "@hapi/hapi": "^20.2.0", - "@koa/router": "^10.1.1", - "@loopback/core": "2.16.2", - "@loopback/repository": "3.7.1", - "@loopback/rest": "9.3.0", - "@types/aws-lambda": "8.10.77", - "@types/co-body": "^5.1.1", - "@types/cookie": "0.3.3", + "@antfu/eslint-config": "^0.35.3", + "@hapi/hapi": "^21.3.0", + "@koa/router": "^12.0.0", + "@loopback/core": "^4.0.8", + "@loopback/repository": "^5.1.3", + "@loopback/rest": "^12.0.8", + "@types/aws-lambda": "^8.10.111", + "@types/body-parser": "^1.19.2", + "@types/co-body": "^6.1.0", + "@types/cookie": "^0.5.1", + "@types/debug": "^4.1.7", "@types/express": "4.16.1", - "@types/hapi__hapi": "20.0.8", - "@types/jsonwebtoken": "9.0.0", + "@types/jsonwebtoken": "^9.0.1", "@types/koa": "^2.13.4", - "@types/koa-bodyparser": "^4.3.3", - "@types/nodemailer": "^6.4.4", - "@types/psl": "1.1.0", - "@types/validator": "10.11.0", + "@types/koa-bodyparser": "^4.3.10", + "@types/koa__router": "^12.0.0", + "@types/node": "^18.15.9", + "@types/nodemailer": "^6.4.7", + "@types/psl": "^1.1.0", + "@types/sinon": "^10.0.13", + "@types/supertest": "^2.0.12", + "@types/validator": "^13.7.13", "aws-sdk-mock": "^5.4.0", - "cookie-parser": "^1.4.5", + "cookie-parser": "^1.4.6", + "eslint": "^8.35.0", "express": "^4.18.2", - "fastify": "3.18.1", - "glob": "7.1.7", - "koa": "^2.13.3", + "fastify": "^4.14.0", + "glob": "^9.2.1", + "koa": "^2.14.1", "lambda-tester": "^4.0.1", - "loopback-datasource-juggler": "^4.26.0", - "mocha": "6.1.4", - "next": "11.1.3", + "loopback-datasource-juggler": "^4.28.2", + "mocha": "^10.2.0", + "next": "^13.2.3", "next-test-api-route-handler": "^3.1.8", - "nock": "11.7.0", - "prettier": "2.0.5", - "pretty-quick": "^3.1.1", - "react": "^17.0.2", - "sinon": "^14.0.0", - "supertest": "4.0.2", - "typedoc": "^0.22.5", - "typescript": "4.2" + "nock": "^13.3.0", + "pretty-quick": "^3.1.3", + "react": "^18.2.0", + "sinon": "^15.0.1", + "supertest": "^6.3.3", + "tslib": "^2.5.0", + "tsup": "^6.6.3", + "typedoc": "^0.23.26", + "typescript": "4.2", + "vite": "^4.1.4", + "vitest": "^0.29.2" }, "browser": { "fs": false + }, + "resolutions": { + "supertokens-node": "link:." } -} +} \ No newline at end of file diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 000000000..51a261956 --- /dev/null +++ b/playground/package.json @@ -0,0 +1,15 @@ +{ + "name": "playground", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "supertokens-node": "workspace:*" + } +} diff --git a/playground/test.ts b/playground/test.ts new file mode 100644 index 000000000..e69de29bb diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..e83f32a2a --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6883 @@ +lockfileVersion: '6.0' + +overrides: + supertokens-node: link:. + +importers: + + .: + dependencies: + '@hapi/boom': + specifier: ^10.0.1 + version: 10.0.1 + axios: + specifier: ^1.3.4 + version: 1.3.4(debug@4.3.4) + body-parser: + specifier: ^1.20.2 + version: 1.20.2 + co-body: + specifier: 6.1.0 + version: 6.1.0 + cookie: + specifier: ^0.5.0 + version: 0.5.0 + debug: + specifier: ^4.3.4 + version: 4.3.4(supports-color@8.1.1) + jsonwebtoken: + specifier: ^9.0.0 + version: 9.0.0 + jwks-rsa: + specifier: ^3.0.1 + version: 3.0.1 + libphonenumber-js: + specifier: ^1.10.21 + version: 1.10.21 + nodemailer: + specifier: ^6.9.1 + version: 6.9.1 + psl: + specifier: ^1.9.0 + version: 1.9.0 + supertokens-js-override: + specifier: ^0.0.4 + version: 0.0.4 + twilio: + specifier: ^4.8.0 + version: 4.8.0(debug@4.3.4) + verify-apple-id-token: + specifier: ^3.0.1 + version: 3.0.1 + devDependencies: + '@antfu/eslint-config': + specifier: ^0.35.3 + version: 0.35.3(eslint@8.35.0)(typescript@4.2.4) + '@hapi/hapi': + specifier: ^21.3.0 + version: 21.3.0 + '@koa/router': + specifier: ^12.0.0 + version: 12.0.0 + '@loopback/core': + specifier: ^4.0.8 + version: 4.0.8 + '@loopback/repository': + specifier: ^5.1.3 + version: 5.1.3(@loopback/core@4.0.8) + '@loopback/rest': + specifier: ^12.0.8 + version: 12.0.8(@loopback/core@4.0.8)(@loopback/repository@5.1.3) + '@types/aws-lambda': + specifier: ^8.10.111 + version: 8.10.111 + '@types/body-parser': + specifier: ^1.19.2 + version: 1.19.2 + '@types/co-body': + specifier: ^6.1.0 + version: 6.1.0 + '@types/cookie': + specifier: ^0.5.1 + version: 0.5.1 + '@types/debug': + specifier: ^4.1.7 + version: 4.1.7 + '@types/express': + specifier: 4.16.1 + version: 4.16.1 + '@types/jsonwebtoken': + specifier: ^9.0.1 + version: 9.0.1 + '@types/koa': + specifier: ^2.13.4 + version: 2.13.5 + '@types/koa-bodyparser': + specifier: ^4.3.10 + version: 4.3.10 + '@types/koa__router': + specifier: ^12.0.0 + version: 12.0.0 + '@types/node': + specifier: ^18.15.9 + version: 18.15.9 + '@types/nodemailer': + specifier: ^6.4.7 + version: 6.4.7 + '@types/psl': + specifier: ^1.1.0 + version: 1.1.0 + '@types/sinon': + specifier: ^10.0.13 + version: 10.0.13 + '@types/supertest': + specifier: ^2.0.12 + version: 2.0.12 + '@types/validator': + specifier: ^13.7.13 + version: 13.7.13 + aws-sdk-mock: + specifier: ^5.4.0 + version: 5.8.0 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.6 + eslint: + specifier: ^8.35.0 + version: 8.35.0 + express: + specifier: ^4.18.2 + version: 4.18.2 + fastify: + specifier: ^4.14.0 + version: 4.14.0 + glob: + specifier: ^9.2.1 + version: 9.2.1 + koa: + specifier: ^2.14.1 + version: 2.14.1 + lambda-tester: + specifier: ^4.0.1 + version: 4.0.1 + loopback-datasource-juggler: + specifier: ^4.28.2 + version: 4.28.2 + mocha: + specifier: ^10.2.0 + version: 10.2.0 + next: + specifier: ^13.2.3 + version: 13.2.4(react-dom@18.2.0)(react@18.2.0) + next-test-api-route-handler: + specifier: ^3.1.8 + version: 3.1.8(next@13.2.4) + nock: + specifier: ^13.3.0 + version: 13.3.0 + pretty-quick: + specifier: ^3.1.3 + version: 3.1.3(prettier@2.8.8) + react: + specifier: ^18.2.0 + version: 18.2.0 + sinon: + specifier: ^15.0.1 + version: 15.0.1 + supertest: + specifier: ^6.3.3 + version: 6.3.3 + tslib: + specifier: ^2.5.0 + version: 2.5.0 + tsup: + specifier: ^6.6.3 + version: 6.6.3(typescript@4.2.4) + typedoc: + specifier: ^0.23.26 + version: 0.23.26(typescript@4.2.4) + typescript: + specifier: '4.2' + version: 4.2.4 + vite: + specifier: ^4.1.4 + version: 4.1.4(@types/node@18.15.9) + vitest: + specifier: ^0.29.2 + version: 0.29.2 + + playground: + dependencies: + supertokens-node: + specifier: link:.. + version: link:.. + +packages: + + /@antfu/eslint-config-basic@0.35.3(@typescript-eslint/eslint-plugin@5.54.0)(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-NbWJKNgd3Ky3/ok2Z88cXNme/6I9otkiaB+FYLFgQE81sfMAhKpLKXtTSwzdcKMzhKDqUchAijt0BxjE/mcTJg==} + peerDependencies: + eslint: '>=7.4.0' + dependencies: + eslint: 8.35.0 + eslint-plugin-antfu: 0.35.3(eslint@8.35.0)(typescript@4.2.4) + eslint-plugin-eslint-comments: 3.2.0(eslint@8.35.0) + eslint-plugin-html: 7.1.0 + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.54.0)(eslint@8.35.0) + eslint-plugin-jsonc: 2.6.0(eslint@8.35.0) + eslint-plugin-markdown: 3.0.0(eslint@8.35.0) + eslint-plugin-n: 15.6.1(eslint@8.35.0) + eslint-plugin-no-only-tests: 3.1.0 + eslint-plugin-promise: 6.1.1(eslint@8.35.0) + eslint-plugin-unicorn: 45.0.2(eslint@8.35.0) + eslint-plugin-unused-imports: 2.0.0(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.35.0) + eslint-plugin-yml: 1.5.0(eslint@8.35.0) + jsonc-eslint-parser: 2.1.0 + yaml-eslint-parser: 1.1.0 + transitivePeerDependencies: + - '@typescript-eslint/eslint-plugin' + - '@typescript-eslint/parser' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + - typescript + dev: true + + /@antfu/eslint-config-ts@0.35.3(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-FS5hir2ghXYlJWAiB2bpT9oAr0kpSNmYbaJWWkztocJG95AORl4tWzxMTkLT+TxaOmhuwJszcrMTHy5RgHL8/w==} + peerDependencies: + eslint: '>=7.4.0' + typescript: '>=3.9' + dependencies: + '@antfu/eslint-config-basic': 0.35.3(@typescript-eslint/eslint-plugin@5.54.0)(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + '@typescript-eslint/eslint-plugin': 5.54.0(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + eslint: 8.35.0 + eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + typescript: 4.2.4 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - jest + - supports-color + dev: true + + /@antfu/eslint-config-vue@0.35.3(@typescript-eslint/eslint-plugin@5.54.0)(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-BA3vGLyuzqtEUb9gfgE7YzBT+a4oUnQuUPasIUfN/BVXaEhRVYlMmUgxN4ekQLuzOgUjUH13lqplXtkLJ62t9g==} + peerDependencies: + eslint: '>=7.4.0' + dependencies: + '@antfu/eslint-config-basic': 0.35.3(@typescript-eslint/eslint-plugin@5.54.0)(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + '@antfu/eslint-config-ts': 0.35.3(eslint@8.35.0)(typescript@4.2.4) + eslint: 8.35.0 + eslint-plugin-vue: 9.9.0(eslint@8.35.0) + local-pkg: 0.4.3 + transitivePeerDependencies: + - '@typescript-eslint/eslint-plugin' + - '@typescript-eslint/parser' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - jest + - supports-color + - typescript + dev: true + + /@antfu/eslint-config@0.35.3(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-wd0ry/TNqaZmniqkKtZKoCvpl55x9YbHgL5Ug3H9rVuUSqaNi9G9AjYlynQqn4/M1EhYYWO597Lu7f/fC+csrg==} + peerDependencies: + eslint: '>=7.4.0' + dependencies: + '@antfu/eslint-config-vue': 0.35.3(@typescript-eslint/eslint-plugin@5.54.0)(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + '@typescript-eslint/eslint-plugin': 5.54.0(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + eslint: 8.35.0 + eslint-plugin-eslint-comments: 3.2.0(eslint@8.35.0) + eslint-plugin-html: 7.1.0 + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.54.0)(eslint@8.35.0) + eslint-plugin-jsonc: 2.6.0(eslint@8.35.0) + eslint-plugin-n: 15.6.1(eslint@8.35.0) + eslint-plugin-promise: 6.1.1(eslint@8.35.0) + eslint-plugin-unicorn: 45.0.2(eslint@8.35.0) + eslint-plugin-vue: 9.9.0(eslint@8.35.0) + eslint-plugin-yml: 1.5.0(eslint@8.35.0) + jsonc-eslint-parser: 2.1.0 + yaml-eslint-parser: 1.1.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - jest + - supports-color + - typescript + dev: true + + /@babel/code-frame@7.12.11: + resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} + dependencies: + '@babel/highlight': 7.18.6 + dev: true + + /@babel/helper-validator-identifier@7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/highlight@7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@esbuild/android-arm64@0.16.17: + resolution: {integrity: sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.17.11: + resolution: {integrity: sha512-QnK4d/zhVTuV4/pRM4HUjcsbl43POALU2zvBynmrrqZt9LPcLA3x1fTZPBg2RRguBQnJcnU059yKr+bydkntjg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.16.17: + resolution: {integrity: sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.17.11: + resolution: {integrity: sha512-CdyX6sRVh1NzFCsf5vw3kULwlAhfy9wVt8SZlrhQ7eL2qBjGbFhRBWkkAzuZm9IIEOCKJw4DXA6R85g+qc8RDw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.16.17: + resolution: {integrity: sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.17.11: + resolution: {integrity: sha512-3PL3HKtsDIXGQcSCKtWD/dy+mgc4p2Tvo2qKgKHj9Yf+eniwFnuoQ0OUhlSfAEpKAFzF9N21Nwgnap6zy3L3MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.16.17: + resolution: {integrity: sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.17.11: + resolution: {integrity: sha512-pJ950bNKgzhkGNO3Z9TeHzIFtEyC2GDQL3wxkMApDEghYx5Qers84UTNc1bAxWbRkuJOgmOha5V0WUeh8G+YGw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.16.17: + resolution: {integrity: sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.17.11: + resolution: {integrity: sha512-iB0dQkIHXyczK3BZtzw1tqegf0F0Ab5texX2TvMQjiJIWXAfM4FQl7D909YfXWnB92OQz4ivBYQ2RlxBJrMJOw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.16.17: + resolution: {integrity: sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.17.11: + resolution: {integrity: sha512-7EFzUADmI1jCHeDRGKgbnF5sDIceZsQGapoO6dmw7r/ZBEKX7CCDnIz8m9yEclzr7mFsd+DyasHzpjfJnmBB1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.16.17: + resolution: {integrity: sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.17.11: + resolution: {integrity: sha512-iPgenptC8i8pdvkHQvXJFzc1eVMR7W2lBPrTE6GbhR54sLcF42mk3zBOjKPOodezzuAz/KSu8CPyFSjcBMkE9g==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.16.17: + resolution: {integrity: sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.17.11: + resolution: {integrity: sha512-Qxth3gsWWGKz2/qG2d5DsW/57SeA2AmpSMhdg9TSB5Svn2KDob3qxfQSkdnWjSd42kqoxIPy3EJFs+6w1+6Qjg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.16.17: + resolution: {integrity: sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.17.11: + resolution: {integrity: sha512-M9iK/d4lgZH0U5M1R2p2gqhPV/7JPJcRz+8O8GBKVgqndTzydQ7B2XGDbxtbvFkvIs53uXTobOhv+RyaqhUiMg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.16.17: + resolution: {integrity: sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.17.11: + resolution: {integrity: sha512-dB1nGaVWtUlb/rRDHmuDQhfqazWE0LMro/AIbT2lWM3CDMHJNpLckH+gCddQyhhcLac2OYw69ikUMO34JLt3wA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.16.17: + resolution: {integrity: sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.17.11: + resolution: {integrity: sha512-aCWlq70Q7Nc9WDnormntGS1ar6ZFvUpqr8gXtO+HRejRYPweAFQN615PcgaSJkZjhHp61+MNLhzyVALSF2/Q0g==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.16.17: + resolution: {integrity: sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.17.11: + resolution: {integrity: sha512-cGeGNdQxqY8qJwlYH1BP6rjIIiEcrM05H7k3tR7WxOLmD1ZxRMd6/QIOWMb8mD2s2YJFNRuNQ+wjMhgEL2oCEw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.16.17: + resolution: {integrity: sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.17.11: + resolution: {integrity: sha512-BdlziJQPW/bNe0E8eYsHB40mYOluS+jULPCjlWiHzDgr+ZBRXPtgMV1nkLEGdpjrwgmtkZHEGEPaKdS/8faLDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.16.17: + resolution: {integrity: sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.17.11: + resolution: {integrity: sha512-MDLwQbtF+83oJCI1Cixn68Et/ME6gelmhssPebC40RdJaect+IM+l7o/CuG0ZlDs6tZTEIoxUe53H3GmMn8oMA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.16.17: + resolution: {integrity: sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.17.11: + resolution: {integrity: sha512-4N5EMESvws0Ozr2J94VoUD8HIRi7X0uvUv4c0wpTHZyZY9qpaaN7THjosdiW56irQ4qnJ6Lsc+i+5zGWnyqWqQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.16.17: + resolution: {integrity: sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.17.11: + resolution: {integrity: sha512-rM/v8UlluxpytFSmVdbCe1yyKQd/e+FmIJE2oPJvbBo+D0XVWi1y/NQ4iTNx+436WmDHQBjVLrbnAQLQ6U7wlw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.16.17: + resolution: {integrity: sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.17.11: + resolution: {integrity: sha512-4WaAhuz5f91h3/g43VBGdto1Q+X7VEZfpcWGtOFXnggEuLvjV+cP6DyLRU15IjiU9fKLLk41OoJfBFN5DhPvag==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.16.17: + resolution: {integrity: sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.17.11: + resolution: {integrity: sha512-UBj135Nx4FpnvtE+C8TWGp98oUgBcmNmdYgl5ToKc0mBHxVVqVE7FUS5/ELMImOp205qDAittL6Ezhasc2Ev/w==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.16.17: + resolution: {integrity: sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.17.11: + resolution: {integrity: sha512-1/gxTifDC9aXbV2xOfCbOceh5AlIidUrPsMpivgzo8P8zUtczlq1ncFpeN1ZyQJ9lVs2hILy1PG5KPp+w8QPPg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.16.17: + resolution: {integrity: sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.17.11: + resolution: {integrity: sha512-vtSfyx5yRdpiOW9yp6Ax0zyNOv9HjOAw8WaZg3dF5djEHKKm3UnoohftVvIJtRh0Ec7Hso0RIdTqZvPXJ7FdvQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.16.17: + resolution: {integrity: sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.17.11: + resolution: {integrity: sha512-GFPSLEGQr4wHFTiIUJQrnJKZhZjjq4Sphf+mM76nQR6WkQn73vm7IsacmBRPkALfpOCHsopSvLgqdd4iUW2mYw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.16.17: + resolution: {integrity: sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.17.11: + resolution: {integrity: sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.2.0(eslint@8.35.0): + resolution: {integrity: sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.35.0 + eslint-visitor-keys: 3.3.0 + dev: true + + /@eslint/eslintrc@2.0.0: + resolution: {integrity: sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4(supports-color@8.1.1) + espree: 9.4.1 + globals: 13.20.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.35.0: + resolution: {integrity: sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@extra-number/significant-digits@1.3.9: + resolution: {integrity: sha512-E5PY/bCwrNqEHh4QS6AQBinLZ+sxM1lT8tsSVYk8VwhWIPp6fCU/BMRVq0V8iJ8LwS3FHmaA4vUzb78s4BIIyA==} + dev: true + + /@fastify/ajv-compiler@3.5.0: + resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-uri: 2.2.0 + dev: true + + /@fastify/deepmerge@1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + dev: true + + /@fastify/error@3.2.0: + resolution: {integrity: sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ==} + dev: true + + /@fastify/fast-json-stringify-compiler@4.2.0: + resolution: {integrity: sha512-ypZynRvXA3dibfPykQN3RB5wBdEUgSGgny8Qc6k163wYPLD4mEGEDkACp+00YmqkGvIm8D/xYoHajwyEdWD/eg==} + dependencies: + fast-json-stringify: 5.6.2 + dev: true + + /@hapi/accept@6.0.1: + resolution: {integrity: sha512-aLkYj7zzgC3CSlEVOs84eBOEE3i9xZK2tdQEP+TOj2OFzMWCi9zjkRet82V3GGjecE//zFrCLKIykuaE0uM4bg==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/ammo@6.0.1: + resolution: {integrity: sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==} + dependencies: + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/b64@6.0.1: + resolution: {integrity: sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==} + dependencies: + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/boom@10.0.1: + resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==} + dependencies: + '@hapi/hoek': 11.0.2 + + /@hapi/bounce@3.0.1: + resolution: {integrity: sha512-G+/Pp9c1Ha4FDP+3Sy/Xwg2O4Ahaw3lIZFSX+BL4uWi64CmiETuZPxhKDUD4xBMOUZbBlzvO8HjiK8ePnhBadA==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/bourne@3.0.0: + resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + dev: true + + /@hapi/call@9.0.1: + resolution: {integrity: sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/catbox-memory@6.0.1: + resolution: {integrity: sha512-sVb+/ZxbZIvaMtJfAbdyY+QJUQg9oKTwamXpEg/5xnfG5WbJLTjvEn4kIGKz9pN3ENNbIL/bIdctmHmqi/AdGA==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/catbox@12.1.1: + resolution: {integrity: sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.2 + '@hapi/podium': 5.0.1 + '@hapi/validate': 2.0.1 + dev: true + + /@hapi/content@6.0.0: + resolution: {integrity: sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==} + dependencies: + '@hapi/boom': 10.0.1 + dev: true + + /@hapi/cryptiles@6.0.1: + resolution: {integrity: sha512-9GM9ECEHfR8lk5ASOKG4+4ZsEzFqLfhiryIJ2ISePVB92OHLp/yne4m+zn7z9dgvM98TLpiFebjDFQ0UHcqxXQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@hapi/boom': 10.0.1 + dev: true + + /@hapi/file@3.0.0: + resolution: {integrity: sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==} + dev: true + + /@hapi/hapi@21.3.0: + resolution: {integrity: sha512-D0g78N1GlbhYteuDFEL3yA3zv/MMQZC9q7U1bblwGNdh1M4oIuy5u3eEeiBSdy7HY8oWhwPCnr0s9287MIctzg==} + engines: {node: '>=14.15.0'} + dependencies: + '@hapi/accept': 6.0.1 + '@hapi/ammo': 6.0.1 + '@hapi/boom': 10.0.1 + '@hapi/bounce': 3.0.1 + '@hapi/call': 9.0.1 + '@hapi/catbox': 12.1.1 + '@hapi/catbox-memory': 6.0.1 + '@hapi/heavy': 8.0.1 + '@hapi/hoek': 11.0.2 + '@hapi/mimos': 7.0.1 + '@hapi/podium': 5.0.1 + '@hapi/shot': 6.0.1 + '@hapi/somever': 4.1.1 + '@hapi/statehood': 8.0.1 + '@hapi/subtext': 8.1.0 + '@hapi/teamwork': 6.0.0 + '@hapi/topo': 6.0.1 + '@hapi/validate': 2.0.1 + dev: true + + /@hapi/heavy@8.0.1: + resolution: {integrity: sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/hoek': 11.0.2 + '@hapi/validate': 2.0.1 + dev: true + + /@hapi/hoek@11.0.2: + resolution: {integrity: sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==} + + /@hapi/iron@7.0.1: + resolution: {integrity: sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==} + dependencies: + '@hapi/b64': 6.0.1 + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/cryptiles': 6.0.1 + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/mimos@7.0.1: + resolution: {integrity: sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==} + dependencies: + '@hapi/hoek': 11.0.2 + mime-db: 1.52.0 + dev: true + + /@hapi/nigel@5.0.1: + resolution: {integrity: sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==} + engines: {node: '>=14.0.0'} + dependencies: + '@hapi/hoek': 11.0.2 + '@hapi/vise': 5.0.1 + dev: true + + /@hapi/pez@6.1.0: + resolution: {integrity: sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg==} + dependencies: + '@hapi/b64': 6.0.1 + '@hapi/boom': 10.0.1 + '@hapi/content': 6.0.0 + '@hapi/hoek': 11.0.2 + '@hapi/nigel': 5.0.1 + dev: true + + /@hapi/podium@5.0.1: + resolution: {integrity: sha512-eznFTw6rdBhAijXFIlBOMJJd+lXTvqbrBIS4Iu80r2KTVIo4g+7fLy4NKp/8+UnSt5Ox6mJtAlKBU/Sf5080TQ==} + dependencies: + '@hapi/hoek': 11.0.2 + '@hapi/teamwork': 6.0.0 + '@hapi/validate': 2.0.1 + dev: true + + /@hapi/shot@6.0.1: + resolution: {integrity: sha512-s5ynMKZXYoDd3dqPw5YTvOR/vjHvMTxc388+0qL0jZZP1+uwXuUD32o9DuuuLsmTlyXCWi02BJl1pBpwRuUrNA==} + dependencies: + '@hapi/hoek': 11.0.2 + '@hapi/validate': 2.0.1 + dev: true + + /@hapi/somever@4.1.1: + resolution: {integrity: sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==} + dependencies: + '@hapi/bounce': 3.0.1 + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/statehood@8.0.1: + resolution: {integrity: sha512-xsKPbouuVX0x5v5TD5FX3m5W5z+HMDutcFkOskien3g7Zo8EtuIAhgtkGxG4voH3OU8PxNz0C6Oxegwoltrsog==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bounce': 3.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/cryptiles': 6.0.1 + '@hapi/hoek': 11.0.2 + '@hapi/iron': 7.0.1 + '@hapi/validate': 2.0.1 + dev: true + + /@hapi/subtext@8.1.0: + resolution: {integrity: sha512-PyaN4oSMtqPjjVxLny1k0iYg4+fwGusIhaom9B2StinBclHs7v46mIW706Y+Wo21lcgulGyXbQrmT/w4dus6ww==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/content': 6.0.0 + '@hapi/file': 3.0.0 + '@hapi/hoek': 11.0.2 + '@hapi/pez': 6.1.0 + '@hapi/wreck': 18.0.1 + dev: true + + /@hapi/teamwork@6.0.0: + resolution: {integrity: sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==} + engines: {node: '>=14.0.0'} + dev: true + + /@hapi/topo@6.0.1: + resolution: {integrity: sha512-JioWUZL1Bm7r8bnCDx2AUggiPwpV7djFfDTWT1aZSyHjN++fVz7XPdW8YVCxvyv9bSWcbbOLV/h4U1zGdwrN3w==} + dependencies: + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/validate@2.0.1: + resolution: {integrity: sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==} + dependencies: + '@hapi/hoek': 11.0.2 + '@hapi/topo': 6.0.1 + dev: true + + /@hapi/vise@5.0.1: + resolution: {integrity: sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==} + dependencies: + '@hapi/hoek': 11.0.2 + dev: true + + /@hapi/wreck@18.0.1: + resolution: {integrity: sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg==} + dependencies: + '@hapi/boom': 10.0.1 + '@hapi/bourne': 3.0.0 + '@hapi/hoek': 11.0.2 + dev: true + + /@humanwhocodes/config-array@0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@koa/router@12.0.0: + resolution: {integrity: sha512-cnnxeKHXlt7XARJptflGURdJaO+ITpNkOHmQu7NHmCoRinPbyvFzce/EG/E8Zy81yQ1W9MoSdtklc3nyaDReUw==} + engines: {node: '>= 12'} + dependencies: + http-errors: 2.0.0 + koa-compose: 4.1.0 + methods: 1.1.2 + path-to-regexp: 6.2.1 + dev: true + + /@loopback/context@5.0.8: + resolution: {integrity: sha512-RJr8TTg5mq0+epEyaaFpV5KkuGsS5AAadyRAdLXxRr6jDBaM9pmQAQZxVTabF8akBkIOW/o1FkSb/8zLpjjyvw==} + engines: {node: 14 || 16 || 18 || 19} + dependencies: + '@loopback/metadata': 5.0.8 + '@types/debug': 4.1.7 + debug: 4.3.4(supports-color@8.1.1) + hyperid: 3.1.1 + p-event: 4.2.0 + tslib: 2.5.0 + uuid: 9.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@loopback/core@4.0.8: + resolution: {integrity: sha512-2Jl62InJFwfybkTm0lZbJrTKEcduyOUI1C3qlfg0ZmBoclvBXTQCjrbBY5gnj5BEqMepJwHkXxRW8SKOVWkMkQ==} + engines: {node: 14 || 16 || 18 || 19} + dependencies: + '@loopback/context': 5.0.8 + debug: 4.3.4(supports-color@8.1.1) + tslib: 2.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@loopback/express@5.0.8(@loopback/core@4.0.8): + resolution: {integrity: sha512-qph2Npcae8WfIv+Ni8cQqFrz9umnWB7nnH1ngDauII6cjzHHQXfUJ+gG/lYVX7D0oqa4gNNZ1PPa1voNqvuTAQ==} + engines: {node: 14 || 16 || 18 || 19} + peerDependencies: + '@loopback/core': ^4.0.8 + dependencies: + '@loopback/core': 4.0.8 + '@loopback/http-server': 4.0.8 + '@types/body-parser': 1.19.2 + '@types/express': 4.17.17 + '@types/express-serve-static-core': 4.17.33 + '@types/http-errors': 2.0.1 + body-parser: 1.20.2 + debug: 4.3.4(supports-color@8.1.1) + express: 4.18.2 + http-errors: 2.0.0 + on-finished: 2.4.1 + toposort: 2.0.2 + tslib: 2.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@loopback/filter@3.0.8: + resolution: {integrity: sha512-SVuFVwZHuOhbhBtRsfy3fKqTao9Py+V91pTQ5YTTA+RxBam2q58bUuF885OPEMOMRA+ZQc7jhwQCEr43mhr65g==} + engines: {node: 14 || 16 || 18 || 19} + dependencies: + tslib: 2.5.0 + dev: true + + /@loopback/http-server@4.0.8: + resolution: {integrity: sha512-P5kmJ1ObxUEqB5ntOmLNmzqeN7GF0ZGFOiYrBhId+/dOE6+FqBkZ5bF9e+xMfpOL/ZeNjmA4USVdQ6BEbVL0iA==} + engines: {node: 14 || 16 || 18 || 19} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + stoppable: 1.1.0 + tslib: 2.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@loopback/metadata@5.0.8: + resolution: {integrity: sha512-ild26/zBK+UIK9t2dwN4MNF0iCdcmHFDa7MWc1MuJlMC+j3p3F8goNsE9oIVyw3eGSWKlGSOBIjwMJX8QBHfzA==} + engines: {node: 14 || 16 || 18 || 19} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + lodash: 4.17.21 + reflect-metadata: 0.1.13 + tslib: 2.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@loopback/openapi-v3@8.0.8(@loopback/core@4.0.8)(@loopback/repository@5.1.3): + resolution: {integrity: sha512-59kULizUN7QtZpq0/2B9ZKz0sM6C9Vp5EzrwEMas8I6MOSAk7TYicwX0mEv7FeNUMoNtPnEKw2yTUDxCnwonCg==} + engines: {node: 14 || 16 || 18 || 19} + peerDependencies: + '@loopback/core': ^4.0.8 + dependencies: + '@loopback/core': 4.0.8 + '@loopback/repository-json-schema': 6.1.2(@loopback/core@4.0.8)(@loopback/repository@5.1.3) + debug: 4.3.4(supports-color@8.1.1) + http-status: 1.6.2 + json-merge-patch: 1.0.2 + lodash: 4.17.21 + openapi3-ts: 2.0.2 + tslib: 2.5.0 + transitivePeerDependencies: + - '@loopback/repository' + - supports-color + dev: true + + /@loopback/repository-json-schema@6.1.2(@loopback/core@4.0.8)(@loopback/repository@5.1.3): + resolution: {integrity: sha512-3NFSUbVyoBCEyFxe2KTS/R/uBdUVRkSDkK3d+L0664LgPV7UhVZJFVUaDn1xngXaNMe3pq6/hCSEXi8G0fXLRg==} + engines: {node: 14 || 16 || 18 || 19} + peerDependencies: + '@loopback/core': ^4.0.8 + '@loopback/repository': ^5.1.3 + dependencies: + '@loopback/core': 4.0.8 + '@loopback/repository': 5.1.3(@loopback/core@4.0.8) + '@types/json-schema': 7.0.11 + debug: 4.3.4(supports-color@8.1.1) + tslib: 2.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@loopback/repository@5.1.3(@loopback/core@4.0.8): + resolution: {integrity: sha512-0kNQEuHDWcY0brpuprhMbXCxUt6M8v4YRBWVTGr9nlRomEPo6BkLOjr0IA5CrzWL77fDcB9BMVusT0Fb0pMncg==} + engines: {node: 14 || 16 || 18 || 19} + peerDependencies: + '@loopback/core': ^4.0.8 + dependencies: + '@loopback/core': 4.0.8 + '@loopback/filter': 3.0.8 + '@types/debug': 4.1.7 + debug: 4.3.4(supports-color@8.1.1) + lodash: 4.17.21 + loopback-datasource-juggler: 4.28.2 + tslib: 2.5.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@loopback/rest@12.0.8(@loopback/core@4.0.8)(@loopback/repository@5.1.3): + resolution: {integrity: sha512-cdCeWiqVvKTCpaTUSCodzxmL4XPbM/5Cdg+l6SVJo50OctNzj9Jy9BwnswCCoZKuJS7m5ikRwWWjFlefWtTAjg==} + engines: {node: 14 || 16 || 18 || 19} + peerDependencies: + '@loopback/core': ^4.0.8 + dependencies: + '@loopback/core': 4.0.8 + '@loopback/express': 5.0.8(@loopback/core@4.0.8) + '@loopback/http-server': 4.0.8 + '@loopback/openapi-v3': 8.0.8(@loopback/core@4.0.8)(@loopback/repository@5.1.3) + '@openapi-contrib/openapi-schema-to-json-schema': 3.2.0 + '@types/body-parser': 1.19.2 + '@types/cors': 2.8.13 + '@types/express': 4.17.17 + '@types/express-serve-static-core': 4.17.33 + '@types/http-errors': 2.0.1 + '@types/on-finished': 2.3.1 + '@types/serve-static': 1.15.0 + '@types/type-is': 1.6.3 + ajv: 8.12.0 + ajv-errors: 3.0.0(ajv@8.12.0) + ajv-formats: 2.1.1(ajv@8.12.0) + ajv-keywords: 5.1.0(ajv@8.12.0) + body-parser: 1.20.2 + cors: 2.8.5 + debug: 4.3.4(supports-color@8.1.1) + express: 4.18.2 + http-errors: 2.0.0 + js-yaml: 4.1.0 + json-schema-compare: 0.2.2 + lodash: 4.17.21 + on-finished: 2.4.1 + path-to-regexp: 6.2.1 + qs: 6.11.0 + strong-error-handler: 4.0.1 + tslib: 2.5.0 + type-is: 1.6.18 + validator: 13.9.0 + transitivePeerDependencies: + - '@loopback/repository' + - supports-color + dev: true + + /@next/env@13.2.4: + resolution: {integrity: sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA==} + dev: true + + /@next/swc-android-arm-eabi@13.2.4: + resolution: {integrity: sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@next/swc-android-arm64@13.2.4: + resolution: {integrity: sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@next/swc-darwin-arm64@13.2.4: + resolution: {integrity: sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@next/swc-darwin-x64@13.2.4: + resolution: {integrity: sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@next/swc-freebsd-x64@13.2.4: + resolution: {integrity: sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@next/swc-linux-arm-gnueabihf@13.2.4: + resolution: {integrity: sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@next/swc-linux-arm64-gnu@13.2.4: + resolution: {integrity: sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@next/swc-linux-arm64-musl@13.2.4: + resolution: {integrity: sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@next/swc-linux-x64-gnu@13.2.4: + resolution: {integrity: sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@next/swc-linux-x64-musl@13.2.4: + resolution: {integrity: sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@next/swc-win32-arm64-msvc@13.2.4: + resolution: {integrity: sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@next/swc-win32-ia32-msvc@13.2.4: + resolution: {integrity: sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@next/swc-win32-x64-msvc@13.2.4: + resolution: {integrity: sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@openapi-contrib/openapi-schema-to-json-schema@3.2.0: + resolution: {integrity: sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw==} + dependencies: + fast-deep-equal: 3.1.3 + dev: true + + /@sinonjs/commons@1.8.6: + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/commons@2.0.0: + resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@10.0.2: + resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} + dependencies: + '@sinonjs/commons': 2.0.0 + dev: true + + /@sinonjs/fake-timers@9.1.2: + resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} + dependencies: + '@sinonjs/commons': 1.8.6 + dev: true + + /@sinonjs/samsam@7.0.1: + resolution: {integrity: sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==} + dependencies: + '@sinonjs/commons': 2.0.0 + lodash.get: 4.4.2 + type-detect: 4.0.8 + dev: true + + /@sinonjs/text-encoding@0.7.2: + resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} + dev: true + + /@swc/helpers@0.4.14: + resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} + dependencies: + tslib: 2.5.0 + dev: true + + /@types/accepts@1.3.5: + resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} + dependencies: + '@types/node': 18.15.9 + dev: true + + /@types/aws-lambda@8.10.111: + resolution: {integrity: sha512-8HR9UjIKmoemEzE2BviVtFkeenxfbizSu8raFjnT2VXxguZZ2JTlNww7INOH7IA0J/zRa3TjOftkYq6hVNkxDA==} + dev: true + + /@types/body-parser@1.19.2: + resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} + dependencies: + '@types/connect': 3.4.35 + '@types/node': 18.15.9 + + /@types/chai-subset@1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.4 + dev: true + + /@types/chai@4.3.4: + resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + dev: true + + /@types/co-body@6.1.0: + resolution: {integrity: sha512-3e0q2jyDAnx/DSZi0z2H0yoZ2wt5yRDZ+P7ymcMObvq0ufWRT4tsajyO+Q1VwVWiv9PRR4W3YEjEzBjeZlhF+w==} + dependencies: + '@types/node': 18.15.9 + '@types/qs': 6.9.7 + dev: true + + /@types/connect@3.4.35: + resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + dependencies: + '@types/node': 18.15.9 + + /@types/content-disposition@0.5.5: + resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==} + dev: true + + /@types/cookie@0.5.1: + resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} + dev: true + + /@types/cookiejar@2.1.2: + resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} + dev: true + + /@types/cookies@0.7.7: + resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==} + dependencies: + '@types/connect': 3.4.35 + '@types/express': 4.16.1 + '@types/keygrip': 1.0.2 + '@types/node': 18.15.9 + dev: true + + /@types/cors@2.8.13: + resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} + dependencies: + '@types/node': 18.15.9 + dev: true + + /@types/debug@4.1.7: + resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} + dependencies: + '@types/ms': 0.7.31 + dev: true + + /@types/express-serve-static-core@4.17.33: + resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} + dependencies: + '@types/node': 18.15.9 + '@types/qs': 6.9.7 + '@types/range-parser': 1.2.4 + + /@types/express@4.16.1: + resolution: {integrity: sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.33 + '@types/serve-static': 1.15.1 + dev: true + + /@types/express@4.17.17: + resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.33 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.1 + + /@types/http-assert@1.5.3: + resolution: {integrity: sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==} + dev: true + + /@types/http-errors@2.0.1: + resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} + dev: true + + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /@types/jsonwebtoken@9.0.1: + resolution: {integrity: sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==} + dependencies: + '@types/node': 18.15.9 + + /@types/keygrip@1.0.2: + resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==} + dev: true + + /@types/koa-bodyparser@4.3.10: + resolution: {integrity: sha512-6ae05pjhmrmGhUR8GYD5qr5p9LTEMEGfGXCsK8VaSL+totwigm8+H/7MHW7K4854CMeuwRAubT8qcc/EagaeIA==} + dependencies: + '@types/koa': 2.13.5 + dev: true + + /@types/koa-compose@3.2.5: + resolution: {integrity: sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==} + dependencies: + '@types/koa': 2.13.5 + dev: true + + /@types/koa@2.13.5: + resolution: {integrity: sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==} + dependencies: + '@types/accepts': 1.3.5 + '@types/content-disposition': 0.5.5 + '@types/cookies': 0.7.7 + '@types/http-assert': 1.5.3 + '@types/http-errors': 2.0.1 + '@types/keygrip': 1.0.2 + '@types/koa-compose': 3.2.5 + '@types/node': 18.15.9 + dev: true + + /@types/koa__router@12.0.0: + resolution: {integrity: sha512-S6eHyZyoWCZLNHyy8j0sMW85cPrpByCbGGU2/BO4IzGiI87aHJ92lZh4E9xfsM9DcbCT469/OIqyC0sSJXSIBQ==} + dependencies: + '@types/koa': 2.13.5 + dev: true + + /@types/mdast@3.0.10: + resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /@types/mime@3.0.1: + resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + + /@types/minimatch@3.0.5: + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + dev: true + + /@types/ms@0.7.31: + resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} + dev: true + + /@types/node@18.15.9: + resolution: {integrity: sha512-dUxhiNzBLr6IqlZXz6e/rN2YQXlFgOei/Dxy+e3cyXTJ4txSUbGT2/fmnD6zd/75jDMeW5bDee+YXxlFKHoV0A==} + + /@types/nodemailer@6.4.7: + resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==} + dependencies: + '@types/node': 18.15.9 + dev: true + + /@types/normalize-package-data@2.4.1: + resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true + + /@types/on-finished@2.3.1: + resolution: {integrity: sha512-mzVYaYcFs5Jd2n/O6uYIRUsFRR1cHyZLRvkLCU0E7+G5WhY0qBDAR5fUCeZbvecYOSh9ikhlesyi2UfI8B9ckQ==} + dependencies: + '@types/node': 18.15.9 + dev: true + + /@types/psl@1.1.0: + resolution: {integrity: sha512-HhZnoLAvI2koev3czVPzBNRYvdrzJGLjQbWZhqFmS9Q6a0yumc5qtfSahBGb5g+6qWvA8iiQktqGkwoIXa/BNQ==} + dev: true + + /@types/qs@6.9.7: + resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + + /@types/range-parser@1.2.4: + resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + + /@types/semver@7.3.13: + resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} + dev: true + + /@types/serve-static@1.15.0: + resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} + dependencies: + '@types/mime': 3.0.1 + '@types/node': 18.15.9 + dev: true + + /@types/serve-static@1.15.1: + resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} + dependencies: + '@types/mime': 3.0.1 + '@types/node': 18.15.9 + + /@types/sinon@10.0.13: + resolution: {integrity: sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==} + dependencies: + '@types/sinonjs__fake-timers': 8.1.2 + dev: true + + /@types/sinonjs__fake-timers@8.1.2: + resolution: {integrity: sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==} + dev: true + + /@types/superagent@4.1.16: + resolution: {integrity: sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==} + dependencies: + '@types/cookiejar': 2.1.2 + '@types/node': 18.15.9 + dev: true + + /@types/supertest@2.0.12: + resolution: {integrity: sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==} + dependencies: + '@types/superagent': 4.1.16 + dev: true + + /@types/type-is@1.6.3: + resolution: {integrity: sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==} + dependencies: + '@types/node': 18.15.9 + dev: true + + /@types/unist@2.0.6: + resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} + dev: true + + /@types/validator@13.7.13: + resolution: {integrity: sha512-EMfHccxNKXaSxTK6DN0En9WsXa7uR4w3LQtx31f6Z2JjG5hJQeVX5zUYMZoatjZgnoQmRcT94WnNWwi0BzQW6Q==} + dev: true + + /@typescript-eslint/eslint-plugin@5.54.0(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + '@typescript-eslint/scope-manager': 5.54.0 + '@typescript-eslint/type-utils': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + '@typescript-eslint/utils': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.35.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + regexpp: 3.2.0 + semver: 7.3.8 + tsutils: 3.21.0(typescript@4.2.4) + typescript: 4.2.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-aAVL3Mu2qTi+h/r04WI/5PfNWvO6pdhpeMRWk9R7rEV4mwJNzoWf5CCU5vDKBsPIFQFjEq1xg7XBI2rjiMXQbQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.54.0 + '@typescript-eslint/types': 5.54.0 + '@typescript-eslint/typescript-estree': 5.54.0(typescript@4.2.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.35.0 + typescript: 4.2.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.54.0: + resolution: {integrity: sha512-VTPYNZ7vaWtYna9M4oD42zENOBrb+ZYyCNdFs949GcN8Miwn37b8b7eMj+EZaq7VK9fx0Jd+JhmkhjFhvnovhg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.54.0 + '@typescript-eslint/visitor-keys': 5.54.0 + dev: true + + /@typescript-eslint/type-utils@5.54.0(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-WI+WMJ8+oS+LyflqsD4nlXMsVdzTMYTxl16myXPaCXnSgc7LWwMsjxQFZCK/rVmTZ3FN71Ct78ehO9bRC7erYQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.54.0(typescript@4.2.4) + '@typescript-eslint/utils': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.35.0 + tsutils: 3.21.0(typescript@4.2.4) + typescript: 4.2.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.54.0: + resolution: {integrity: sha512-nExy+fDCBEgqblasfeE3aQ3NuafBUxZxgxXcYfzYRZFHdVvk5q60KhCSkG0noHgHRo/xQ/BOzURLZAafFpTkmQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.54.0(typescript@4.2.4): + resolution: {integrity: sha512-X2rJG97Wj/VRo5YxJ8Qx26Zqf0RRKsVHd4sav8NElhbZzhpBI8jU54i6hfo9eheumj4oO4dcRN1B/zIVEqR/MQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.54.0 + '@typescript-eslint/visitor-keys': 5.54.0 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.3.8 + tsutils: 3.21.0(typescript@4.2.4) + typescript: 4.2.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.54.0(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-cuwm8D/Z/7AuyAeJ+T0r4WZmlnlxQ8wt7C7fLpFlKMR+dY6QO79Cq1WpJhvZbMA4ZeZGHiRWnht7ZJ8qkdAunw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.54.0 + '@typescript-eslint/types': 5.54.0 + '@typescript-eslint/typescript-estree': 5.54.0(typescript@4.2.4) + eslint: 8.35.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.35.0) + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.54.0: + resolution: {integrity: sha512-xu4wT7aRCakGINTLGeyGqDn+78BwFlggwBjnHa1ar/KaGagnmwLYmlrXIrgAaQ3AE1Vd6nLfKASm7LrFHNbKGA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.54.0 + eslint-visitor-keys: 3.3.0 + dev: true + + /@vitest/expect@0.29.2: + resolution: {integrity: sha512-wjrdHB2ANTch3XKRhjWZN0UueFocH0cQbi2tR5Jtq60Nb3YOSmakjdAvUa2JFBu/o8Vjhj5cYbcMXkZxn1NzmA==} + dependencies: + '@vitest/spy': 0.29.2 + '@vitest/utils': 0.29.2 + chai: 4.3.7 + dev: true + + /@vitest/runner@0.29.2: + resolution: {integrity: sha512-A1P65f5+6ru36AyHWORhuQBJrOOcmDuhzl5RsaMNFe2jEkoj0faEszQS4CtPU/LxUYVIazlUtZTY0OEZmyZBnA==} + dependencies: + '@vitest/utils': 0.29.2 + p-limit: 4.0.0 + pathe: 1.1.0 + dev: true + + /@vitest/spy@0.29.2: + resolution: {integrity: sha512-Hc44ft5kaAytlGL2PyFwdAsufjbdOvHklwjNy/gy/saRbg9Kfkxfh+PklLm1H2Ib/p586RkQeNFKYuJInUssyw==} + dependencies: + tinyspy: 1.1.1 + dev: true + + /@vitest/utils@0.29.2: + resolution: {integrity: sha512-F14/Uc+vCdclStS2KEoXJlOLAEyqRhnw0gM27iXw9bMTcyKRPJrQ+rlC6XZ125GIPvvKYMPpVxNhiou6PsEeYQ==} + dependencies: + cli-truncate: 3.1.0 + diff: 5.1.0 + loupe: 2.3.6 + picocolors: 1.0.0 + pretty-format: 27.5.1 + dev: true + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + + /abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + dev: true + + /accept-language@3.0.18: + resolution: {integrity: sha512-sUofgqBPzgfcF20sPoBYGQ1IhQLt2LSkxTnlQSuLF3n5gPEqd5AimbvOvHEi0T1kLMiGVqPWzI5a9OteBRth3A==} + dependencies: + bcp47: 1.1.2 + stable: 0.1.8 + dev: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: true + + /acorn-jsx@5.3.2(acorn@8.8.2): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.2 + dev: true + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: false + + /ajv-errors@3.0.0(ajv@8.12.0): + resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} + peerDependencies: + ajv: ^8.0.1 + dependencies: + ajv: 8.12.0 + dev: true + + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: true + + /ajv-keywords@5.1.0(ajv@8.12.0): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + dependencies: + ajv: 8.12.0 + fast-deep-equal: 3.1.3 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-colors@4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-sequence-parser@1.1.0: + resolution: {integrity: sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + dev: true + + /archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-differ@3.0.0: + resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} + engines: {node: '>=8'} + dev: true + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: true + + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + get-intrinsic: 1.2.0 + is-string: 1.0.7 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.flat@1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + es-shim-unscopables: 1.0.0 + dev: true + + /arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + dev: true + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: true + + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /avvio@8.2.1: + resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} + dependencies: + archy: 1.0.0 + debug: 4.3.4(supports-color@8.1.1) + fastq: 1.15.0 + transitivePeerDependencies: + - supports-color + dev: true + + /aws-sdk-mock@5.8.0: + resolution: {integrity: sha512-s0Vy4DObFmVJ6h1uTw1LGInOop77oF0JXH2N39Lv+1Wss274EowVk9odhM4Sji4mynXcM5oSu68uYqkJRviDRA==} + dependencies: + aws-sdk: 2.1327.0 + sinon: 14.0.2 + traverse: 0.6.7 + dev: true + + /aws-sdk@2.1327.0: + resolution: {integrity: sha512-adyoVv5MGGyq6Gm2k/W2h1dqmtMw+td5IW86vomKtMTT0S0eI2iYNABCk9G2EBqZOq8nx6RYuEyhascN7eaaig==} + engines: {node: '>= 10.0.0'} + dependencies: + buffer: 4.9.2 + events: 1.1.1 + ieee754: 1.1.13 + jmespath: 0.16.0 + querystring: 0.2.0 + sax: 1.2.1 + url: 0.10.3 + util: 0.12.5 + uuid: 8.0.0 + xml2js: 0.4.19 + dev: true + + /axios@0.26.1(debug@4.3.4): + resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} + dependencies: + follow-redirects: 1.15.2(debug@4.3.4) + transitivePeerDependencies: + - debug + dev: false + + /axios@1.3.4(debug@4.3.4): + resolution: {integrity: sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==} + dependencies: + follow-redirects: 1.15.2(debug@4.3.4) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /bcp47@1.1.2: + resolution: {integrity: sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==} + engines: {node: '>=0.10'} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /bl@2.2.1: + resolution: {integrity: sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==} + dependencies: + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + dev: true + + /bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + dev: true + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /builtins@5.0.1: + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + dependencies: + semver: 7.3.8 + dev: true + + /bundle-require@4.0.1(esbuild@0.17.11): + resolution: {integrity: sha512-9NQkRHlNdNpDBGmLpngF3EFDcwodhMUuLz9PaWYciVcQF9SE4LFjM2DB/xV1Li5JiuDMv7ZUWuC3rGbqR0MAXQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + dependencies: + esbuild: 0.17.11 + load-tsconfig: 0.2.3 + dev: true + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /cache-content-type@1.0.1: + resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} + engines: {node: '>= 6.0.0'} + dependencies: + mime-types: 2.1.35 + ylru: 1.3.2 + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.0 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + dependencies: + pascal-case: 3.1.2 + tslib: 2.5.0 + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /caniuse-lite@1.0.30001460: + resolution: {integrity: sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ==} + dev: true + + /capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + upper-case-first: 2.0.2 + dev: true + + /chai@4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /change-case@4.1.2: + resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + dependencies: + camel-case: 4.1.2 + capital-case: 1.0.4 + constant-case: 3.0.4 + dot-case: 3.0.4 + header-case: 2.0.4 + no-case: 3.0.4 + param-case: 3.0.4 + pascal-case: 3.1.2 + path-case: 3.0.4 + sentence-case: 3.0.4 + snake-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + dev: true + + /character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + dev: true + + /character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + dev: true + + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: true + + /check-error@1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + + /chokidar@3.5.1: + resolution: {integrity: sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.5.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /ci-info@3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + dev: true + + /cldrjs@0.5.5: + resolution: {integrity: sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==} + dev: true + + /clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /cli-truncate@3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + dev: true + + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + dev: true + + /co-body@6.1.0: + resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} + dependencies: + inflation: 2.0.0 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /component-emitter@1.3.0: + resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /constant-case@3.0.4: + resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + upper-case: 2.0.2 + dev: true + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + /cookie-parser@1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: true + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: true + + /cookie@0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: true + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + /cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + dev: true + + /cookies@0.8.0: + resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /dayjs@1.11.7: + resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /debug@4.3.4(supports-color@8.1.1): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + + /decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-equal@1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: true + + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: true + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + /dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + dev: true + + /diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: true + + /diff@5.1.0: + resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.4.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.0.1: + resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /dotenv-json@1.0.0: + resolution: {integrity: sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ==} + dev: true + + /dotenv@8.6.0: + resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} + engines: {node: '>=10'} + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + /ejs@3.1.8: + resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.5 + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /entities@4.4.0: + resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /es-abstract@1.21.1: + resolution: {integrity: sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function-bind: 1.1.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.0 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.10 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + safe-regex-test: 1.0.0 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.16.17: + resolution: {integrity: sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.16.17 + '@esbuild/android-arm64': 0.16.17 + '@esbuild/android-x64': 0.16.17 + '@esbuild/darwin-arm64': 0.16.17 + '@esbuild/darwin-x64': 0.16.17 + '@esbuild/freebsd-arm64': 0.16.17 + '@esbuild/freebsd-x64': 0.16.17 + '@esbuild/linux-arm': 0.16.17 + '@esbuild/linux-arm64': 0.16.17 + '@esbuild/linux-ia32': 0.16.17 + '@esbuild/linux-loong64': 0.16.17 + '@esbuild/linux-mips64el': 0.16.17 + '@esbuild/linux-ppc64': 0.16.17 + '@esbuild/linux-riscv64': 0.16.17 + '@esbuild/linux-s390x': 0.16.17 + '@esbuild/linux-x64': 0.16.17 + '@esbuild/netbsd-x64': 0.16.17 + '@esbuild/openbsd-x64': 0.16.17 + '@esbuild/sunos-x64': 0.16.17 + '@esbuild/win32-arm64': 0.16.17 + '@esbuild/win32-ia32': 0.16.17 + '@esbuild/win32-x64': 0.16.17 + dev: true + + /esbuild@0.17.11: + resolution: {integrity: sha512-pAMImyokbWDtnA/ufPxjQg0fYo2DDuzAlqwnDvbXqHLphe+m80eF++perYKVm8LeTuj2zUuFXC+xgSVxyoHUdg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.11 + '@esbuild/android-arm64': 0.17.11 + '@esbuild/android-x64': 0.17.11 + '@esbuild/darwin-arm64': 0.17.11 + '@esbuild/darwin-x64': 0.17.11 + '@esbuild/freebsd-arm64': 0.17.11 + '@esbuild/freebsd-x64': 0.17.11 + '@esbuild/linux-arm': 0.17.11 + '@esbuild/linux-arm64': 0.17.11 + '@esbuild/linux-ia32': 0.17.11 + '@esbuild/linux-loong64': 0.17.11 + '@esbuild/linux-mips64el': 0.17.11 + '@esbuild/linux-ppc64': 0.17.11 + '@esbuild/linux-riscv64': 0.17.11 + '@esbuild/linux-s390x': 0.17.11 + '@esbuild/linux-x64': 0.17.11 + '@esbuild/netbsd-x64': 0.17.11 + '@esbuild/openbsd-x64': 0.17.11 + '@esbuild/sunos-x64': 0.17.11 + '@esbuild/win32-arm64': 0.17.11 + '@esbuild/win32-ia32': 0.17.11 + '@esbuild/win32-x64': 0.17.11 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-import-resolver-node@0.3.7: + resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} + dependencies: + debug: 3.2.7 + is-core-module: 2.11.0 + resolve: 1.22.1 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.54.0)(eslint-import-resolver-node@0.3.7)(eslint@8.35.0): + resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + debug: 3.2.7 + eslint: 8.35.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-antfu@0.35.3(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-90Xct24s2n3aQhuuFFcPLhF5E6lU5s225B0VXupSjvDTuF+CmSQQLQG6KcqcdpA8O6dMbeXB9zy3SJ4aO7lndw==} + dependencies: + '@typescript-eslint/utils': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + dev: true + + /eslint-plugin-es@4.1.0(eslint@8.35.0): + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.35.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + + /eslint-plugin-eslint-comments@3.2.0(eslint@8.35.0): + resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==} + engines: {node: '>=6.5.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + escape-string-regexp: 1.0.5 + eslint: 8.35.0 + ignore: 5.2.4 + dev: true + + /eslint-plugin-html@7.1.0: + resolution: {integrity: sha512-fNLRraV/e6j8e3XYOC9xgND4j+U7b1Rq+OygMlLcMg+wI/IpVbF+ubQa3R78EjKB9njT6TQOlcK5rFKBVVtdfg==} + dependencies: + htmlparser2: 8.0.1 + dev: true + + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.54.0)(eslint@8.35.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.35.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.54.0)(eslint-import-resolver-node@0.3.7)(eslint@8.35.0) + has: 1.0.3 + is-core-module: 2.11.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.1 + semver: 6.3.0 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-plugin-jest@27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.35.0)(typescript@4.2.4): + resolution: {integrity: sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.54.0(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + '@typescript-eslint/utils': 5.54.0(eslint@8.35.0)(typescript@4.2.4) + eslint: 8.35.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /eslint-plugin-jsonc@2.6.0(eslint@8.35.0): + resolution: {integrity: sha512-4bA9YTx58QaWalua1Q1b82zt7eZMB7i+ed8q8cKkbKP75ofOA2SXbtFyCSok7RY6jIXeCqQnKjN9If8zCgv6PA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + eslint: 8.35.0 + eslint-utils: 3.0.0(eslint@8.35.0) + jsonc-eslint-parser: 2.1.0 + natural-compare: 1.4.0 + dev: true + + /eslint-plugin-markdown@3.0.0(eslint@8.35.0): + resolution: {integrity: sha512-hRs5RUJGbeHDLfS7ELanT0e29Ocyssf/7kBM+p7KluY5AwngGkDf8Oyu4658/NZSGTTq05FZeWbkxXtbVyHPwg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.35.0 + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-n@15.6.1(eslint@8.35.0): + resolution: {integrity: sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==} + engines: {node: '>=12.22.0'} + peerDependencies: + eslint: '>=7.0.0' + dependencies: + builtins: 5.0.1 + eslint: 8.35.0 + eslint-plugin-es: 4.1.0(eslint@8.35.0) + eslint-utils: 3.0.0(eslint@8.35.0) + ignore: 5.2.4 + is-core-module: 2.11.0 + minimatch: 3.1.2 + resolve: 1.22.1 + semver: 7.3.8 + dev: true + + /eslint-plugin-no-only-tests@3.1.0: + resolution: {integrity: sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==} + engines: {node: '>=5.0.0'} + dev: true + + /eslint-plugin-promise@6.1.1(eslint@8.35.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.35.0 + dev: true + + /eslint-plugin-unicorn@45.0.2(eslint@8.35.0): + resolution: {integrity: sha512-Y0WUDXRyGDMcKLiwgL3zSMpHrXI00xmdyixEGIg90gHnj0PcHY4moNv3Ppje/kDivdAy5vUeUr7z211ImPv2gw==} + engines: {node: '>=14.18'} + peerDependencies: + eslint: '>=8.28.0' + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + '@eslint-community/eslint-utils': 4.2.0(eslint@8.35.0) + ci-info: 3.8.0 + clean-regexp: 1.0.0 + eslint: 8.35.0 + esquery: 1.5.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.0.2 + lodash: 4.17.21 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.24 + regjsparser: 0.9.1 + safe-regex: 2.1.1 + semver: 7.3.8 + strip-indent: 3.0.0 + dev: true + + /eslint-plugin-unused-imports@2.0.0(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.35.0): + resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.54.0(@typescript-eslint/parser@5.54.0)(eslint@8.35.0)(typescript@4.2.4) + eslint: 8.35.0 + eslint-rule-composer: 0.3.0 + dev: true + + /eslint-plugin-vue@9.9.0(eslint@8.35.0): + resolution: {integrity: sha512-YbubS7eK0J7DCf0U2LxvVP7LMfs6rC6UltihIgval3azO3gyDwEGVgsCMe1TmDiEkl6GdMKfRpaME6QxIYtzDQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.35.0 + eslint-utils: 3.0.0(eslint@8.35.0) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.0.11 + semver: 7.3.8 + vue-eslint-parser: 9.1.0(eslint@8.35.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-yml@1.5.0(eslint@8.35.0): + resolution: {integrity: sha512-iygN054g+ZrnYmtOXMnT+sx9iDNXt89/m0+506cQHeG0+5jJN8hY5iOPQLd3yfd50AfK/mSasajBWruf1SoHpQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.35.0 + lodash: 4.17.21 + natural-compare: 1.4.0 + yaml-eslint-parser: 1.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-rule-composer@0.3.0: + resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} + engines: {node: '>=4.0.0'} + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.1.1: + resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-utils@3.0.0(eslint@8.35.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.35.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.3.0: + resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.35.0: + resolution: {integrity: sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint/eslintrc': 2.0.0 + '@eslint/js': 8.35.0 + '@humanwhocodes/config-array': 0.11.8 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.1.1 + eslint-utils: 3.0.0(eslint@8.35.0) + eslint-visitor-keys: 3.3.0 + espree: 9.4.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-sdsl: 4.3.0 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.4.1: + resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) + eslint-visitor-keys: 3.3.0 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: true + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + + /events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /fast-content-type-parse@1.0.0: + resolution: {integrity: sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==} + dev: true + + /fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-json-stringify@5.6.2: + resolution: {integrity: sha512-F6xkRrXvtGbAiDSEI5Rk7qk2P63Y9kc8bO6Dnsd3Rt6sBNr2QxNFWs0JbKftgiyOfGxnJaRoHe4SizCTqeAyrA==} + dependencies: + '@fastify/deepmerge': 1.3.0 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-deep-equal: 3.1.3 + fast-uri: 2.2.0 + rfdc: 1.3.0 + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-querystring@1.1.1: + resolution: {integrity: sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==} + dependencies: + fast-decode-uri-component: 1.0.1 + dev: true + + /fast-redact@3.1.2: + resolution: {integrity: sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==} + engines: {node: '>=6'} + dev: true + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + + /fast-uri@2.2.0: + resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} + dev: true + + /fastify@4.14.0: + resolution: {integrity: sha512-oJSHlM/XbGdJpe2MKMJBsrvrkPDrHDZlAB9qzuUJIpnBtpDE394bzdFsH4KnsUI1e8zxzFl+GNBEXC64N/IPuw==} + dependencies: + '@fastify/ajv-compiler': 3.5.0 + '@fastify/error': 3.2.0 + '@fastify/fast-json-stringify-compiler': 4.2.0 + abstract-logging: 2.0.1 + avvio: 8.2.1 + fast-content-type-parse: 1.0.0 + find-my-way: 7.5.0 + light-my-request: 5.9.1 + pino: 8.11.0 + process-warning: 2.1.0 + proxy-addr: 2.0.7 + rfdc: 1.3.0 + secure-json-parse: 2.7.0 + semver: 7.3.8 + tiny-lru: 10.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /find-my-way@7.5.0: + resolution: {integrity: sha512-3ehydSBhGcS0TtMA/BYEyMAKi9Sv0MqF8aqiMO5oGBXyCcSlyEJyfGWsbNxAx7BekTNWUwD1ttLJLURni2vmJg==} + engines: {node: '>=14'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.1 + safe-regex2: 2.0.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + dev: true + + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /flatted@3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true + + /follow-redirects@1.15.2(debug@4.3.4): + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.4(supports-color@8.1.1) + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + /formidable@2.1.2: + resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + dependencies: + dezalgo: 1.0.4 + hexoid: 1.0.0 + once: 1.4.0 + qs: 6.11.0 + dev: true + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: true + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-func-name@2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + + /get-intrinsic@1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@9.2.1: + resolution: {integrity: sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 7.4.2 + minipass: 4.2.4 + path-scurry: 1.6.1 + dev: true + + /globalize@1.7.0: + resolution: {integrity: sha512-faR46vTIbFCeAemyuc9E6/d7Wrx9k2ae2L60UhakztFg6VuE42gENVJNuPFtt7Sdjrk9m2w8+py7Jj+JTNy59w==} + dependencies: + cldrjs: 0.5.5 + dev: true + + /globals@13.20.0: + resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.0 + dev: true + + /grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.0 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /header-case@2.0.4: + resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + dependencies: + capital-case: 1.0.4 + tslib: 2.5.0 + dev: true + + /hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + dev: true + + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /htmlparser2@8.0.1: + resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.0.1 + entities: 4.4.0 + dev: true + + /http-assert@1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + dev: true + + /http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + dev: true + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + /http-status@1.6.2: + resolution: {integrity: sha512-oUExvfNckrpTpDazph7kNG8sQi5au3BeTo0idaZFXEhTaJKu7GNJCLHI0rYY2wljm548MSTM+Ljj/c6anqu2zQ==} + engines: {node: '>= 0.4.0'} + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: false + + /human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /hyperid@3.1.1: + resolution: {integrity: sha512-RveV33kIksycSf7HLkq1sHB5wW0OwuX8ot8MYnY++gaaPXGFfKpBncHrAWxdpuEeRlazUMGWefwP1w6o6GaumA==} + dependencies: + uuid: 8.3.2 + uuid-parse: 1.1.0 + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + + /ieee754@1.1.13: + resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflation@2.0.0: + resolution: {integrity: sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==} + engines: {node: '>= 0.8.0'} + dev: false + + /inflection@1.13.4: + resolution: {integrity: sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==} + engines: {'0': node >= 0.4.0} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /invert-kv@3.0.1: + resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==} + engines: {node: '>=8'} + dev: true + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: true + + /is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + dev: true + + /is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + dev: true + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-typed-array: 1.1.10 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: true + + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: true + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: true + + /jake@10.8.5: + resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.4 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: true + + /jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + dev: true + + /jose@4.13.1: + resolution: {integrity: sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==} + dev: false + + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + + /js-sdsl@4.3.0: + resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /js2xmlparser@4.0.2: + resolution: {integrity: sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==} + dependencies: + xmlcreate: 2.0.4 + dev: true + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /json-merge-patch@1.0.2: + resolution: {integrity: sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==} + dependencies: + fast-deep-equal: 3.1.3 + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-compare@0.2.2: + resolution: {integrity: sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==} + dependencies: + lodash: 4.17.21 + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + + /jsonc-eslint-parser@2.1.0: + resolution: {integrity: sha512-qCRJWlbP2v6HbmKW7R3lFbeiVWHo+oMJ0j+MizwvauqnCV/EvtAeEeuCgoc/ErtsuoKgYB8U4Ih8AxJbXoE6/g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.2 + eslint-visitor-keys: 3.3.0 + espree: 9.4.1 + semver: 7.3.8 + dev: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonwebtoken@9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.3.8 + dev: false + + /just-extend@4.2.1: + resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} + dev: true + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jwks-rsa@3.0.1: + resolution: {integrity: sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==} + engines: {node: '>=14'} + dependencies: + '@types/express': 4.17.17 + '@types/jsonwebtoken': 9.0.1 + debug: 4.3.4(supports-color@8.1.1) + jose: 4.13.1 + limiter: 1.1.5 + lru-memoizer: 2.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + dependencies: + tsscmp: 1.0.6 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /koa-compose@4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + dev: true + + /koa-convert@2.0.0: + resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} + engines: {node: '>= 10'} + dependencies: + co: 4.6.0 + koa-compose: 4.1.0 + dev: true + + /koa@2.14.1: + resolution: {integrity: sha512-USJFyZgi2l0wDgqkfD27gL4YGno7TfUkcmOe6UOLFOVuN+J7FwnNu4Dydl4CUQzraM1lBAiGed0M9OVJoT0Kqw==} + engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} + dependencies: + accepts: 1.3.8 + cache-content-type: 1.0.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.8.0 + debug: 4.3.4(supports-color@8.1.1) + delegates: 1.0.0 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.8.1 + is-generator-function: 1.0.10 + koa-compose: 4.1.0 + koa-convert: 2.0.0 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /lambda-event-mock@1.5.0: + resolution: {integrity: sha512-vx1d+vZqi7FF6B3+mAfHnY/6Tlp6BheL2ta0MJS0cIRB3Rc4I5cviHTkiJxHdE156gXx3ZjlQRJrS4puXvtrhA==} + engines: {node: '>=12.13'} + dependencies: + '@extra-number/significant-digits': 1.3.9 + clone-deep: 4.0.1 + uuid: 3.4.0 + vandium-utils: 1.2.0 + dev: true + + /lambda-leak@2.0.0: + resolution: {integrity: sha512-2c9jwUN3ZLa2GEiOhObbx2BMGQplEUCDHSIkhDtYwUjsTfiV/3jCF6ThIuEXfsvqbUK+0QpZcugIKB8YMbSevQ==} + engines: {node: '>=6.10.0'} + dev: true + + /lambda-tester@4.0.1: + resolution: {integrity: sha512-ft6XHk84B6/dYEzyI3anKoGWz08xQ5allEHiFYDUzaYTymgVK7tiBkCEbuWx+MFvH7OpFNsJXVtjXm0X8iH3Iw==} + engines: {node: '>=10.0'} + dependencies: + app-root-path: 3.1.0 + dotenv: 8.6.0 + dotenv-json: 1.0.0 + lambda-event-mock: 1.5.0 + lambda-leak: 2.0.0 + semver: 6.3.0 + uuid: 3.4.0 + vandium-utils: 2.0.0 + dev: true + + /lcid@3.1.1: + resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==} + engines: {node: '>=8'} + dependencies: + invert-kv: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /libphonenumber-js@1.10.21: + resolution: {integrity: sha512-/udZhx49av2r2gZR/+xXSrwcR8smX/sDNrVpOFrvW+CA26TfYTVZfwb3MIDvmwAYMLs7pXuJjZX0VxxGpqPhsA==} + dev: false + + /light-my-request@5.9.1: + resolution: {integrity: sha512-UT7pUk8jNCR1wR7w3iWfIjx32DiB2f3hFdQSOwy3/EPQ3n3VocyipUxcyRZR0ahoev+fky69uA+GejPa9KuHKg==} + dependencies: + cookie: 0.5.0 + process-warning: 2.1.0 + set-cookie-parser: 2.5.1 + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + dev: false + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /load-tsconfig@0.2.3: + resolution: {integrity: sha512-iyT2MXws+dc2Wi6o3grCFtGXpeMvHmJqS27sMPGtV2eUu4PeFnG+33I8BlFK1t1NWMjOpcx9bridn5yxLDX2gQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /loopback-connector@5.2.1: + resolution: {integrity: sha512-jWCjljtMSe+pZV5X5pYQOg2Gt3DjiC4O9dha2lXdXigS9rrhZbrBrHL8leA+qnYrexcoEPwL5Pcxc0AqVwT2bw==} + engines: {node: '>=10'} + dependencies: + async: 3.2.4 + bluebird: 3.7.2 + debug: 4.3.4(supports-color@8.1.1) + msgpack5: 4.5.1 + strong-globalize: 6.0.5 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + dev: true + + /loopback-datasource-juggler@4.28.2: + resolution: {integrity: sha512-3+NtxehBDPWmRNFMm34JceoOSmdkGcDrToZVHqhjCtxJJ+M/3KSV0ObwD6pD+eA27liKg09Rfp4oezjw6I/ZOg==} + engines: {node: '>=10'} + dependencies: + async: 3.2.4 + change-case: 4.1.2 + debug: 4.3.4(supports-color@8.1.1) + depd: 2.0.0 + inflection: 1.13.4 + lodash: 4.17.21 + loopback-connector: 5.2.1 + minimatch: 5.1.6 + nanoid: 3.3.4 + qs: 6.11.0 + strong-globalize: 6.0.5 + traverse: 0.6.7 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: true + + /loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.5.0 + dev: true + + /lru-cache@4.0.2: + resolution: {integrity: sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /lru-cache@7.18.1: + resolution: {integrity: sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg==} + engines: {node: '>=12'} + dev: true + + /lru-memoizer@2.2.0: + resolution: {integrity: sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==} + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 4.0.2 + dev: false + + /lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: true + + /map-age-cleaner@0.1.3: + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} + dependencies: + p-defer: 1.0.0 + dev: true + + /marked@4.2.12: + resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==} + engines: {node: '>= 12'} + hasBin: true + dev: true + + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: true + + /mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + dependencies: + '@types/mdast': 3.0.10 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + /mem@5.1.1: + resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==} + engines: {node: '>=8'} + dependencies: + map-age-cleaner: 0.1.3 + mimic-fn: 2.1.0 + p-is-promise: 2.1.0 + dev: true + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: true + + /micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@7.4.2: + resolution: {integrity: sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /minipass@4.2.4: + resolution: {integrity: sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==} + engines: {node: '>=8'} + dev: true + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /mlly@1.1.1: + resolution: {integrity: sha512-Jnlh4W/aI4GySPo6+DyTN17Q75KKbLTyFK8BrGhjNP4rxuUjbRWhE6gHg3bs33URWAF44FRm7gdQA348i3XxRw==} + dependencies: + acorn: 8.8.2 + pathe: 1.1.0 + pkg-types: 1.0.2 + ufo: 1.1.1 + dev: true + + /mocha@10.2.0: + resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==} + engines: {node: '>= 14.0.0'} + hasBin: true + dependencies: + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4(supports-color@8.1.1) + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + nanoid: 3.3.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: true + + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /msgpack5@4.5.1: + resolution: {integrity: sha512-zC1vkcliryc4JGlL6OfpHumSYUHWFGimSI+OgfRCjTFLmKA2/foR9rMTOhWiqfOrfxJOctrpWPvrppf8XynJxw==} + dependencies: + bl: 2.2.1 + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + dev: true + + /multimatch@4.0.0: + resolution: {integrity: sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==} + engines: {node: '>=8'} + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 3.0.0 + array-union: 2.1.0 + arrify: 2.0.1 + minimatch: 3.1.2 + dev: true + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /nanoid@3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: true + + /next-test-api-route-handler@3.1.8(next@13.2.4): + resolution: {integrity: sha512-8o+TjtlQdvLv7YmR5NOl+AQCM/tEZqB1mvqBeGFQscuGtL+42fOrOsDFv2QsFUWieUMKv7j7NqOmYMpJRPu3/w==} + engines: {node: '>=12'} + peerDependencies: + next: '>=9' + dependencies: + cookie: 0.5.0 + next: 13.2.4(react-dom@18.2.0)(react@18.2.0) + node-fetch: 2.6.9 + transitivePeerDependencies: + - encoding + dev: true + + /next@13.2.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==} + engines: {node: '>=14.6.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.4.0 + fibers: '>= 3.1.0' + node-sass: ^6.0.0 || ^7.0.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.2.4 + '@swc/helpers': 0.4.14 + caniuse-lite: 1.0.30001460 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + optionalDependencies: + '@next/swc-android-arm-eabi': 13.2.4 + '@next/swc-android-arm64': 13.2.4 + '@next/swc-darwin-arm64': 13.2.4 + '@next/swc-darwin-x64': 13.2.4 + '@next/swc-freebsd-x64': 13.2.4 + '@next/swc-linux-arm-gnueabihf': 13.2.4 + '@next/swc-linux-arm64-gnu': 13.2.4 + '@next/swc-linux-arm64-musl': 13.2.4 + '@next/swc-linux-x64-gnu': 13.2.4 + '@next/swc-linux-x64-musl': 13.2.4 + '@next/swc-win32-arm64-msvc': 13.2.4 + '@next/swc-win32-ia32-msvc': 13.2.4 + '@next/swc-win32-x64-msvc': 13.2.4 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: true + + /nise@5.1.4: + resolution: {integrity: sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 10.0.2 + '@sinonjs/text-encoding': 0.7.2 + just-extend: 4.2.1 + path-to-regexp: 1.8.0 + dev: true + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.5.0 + dev: true + + /nock@13.3.0: + resolution: {integrity: sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg==} + engines: {node: '>= 10.13'} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + json-stringify-safe: 5.0.1 + lodash: 4.17.21 + propagate: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /node-fetch@2.6.9: + resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: true + + /nodemailer@6.9.1: + resolution: {integrity: sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==} + engines: {node: '>=6.0.0'} + dev: false + + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.1 + semver: 5.7.1 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.values@1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + dev: true + + /on-exit-leak-free@2.1.0: + resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} + dev: true + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /only@0.0.2: + resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} + dev: true + + /openapi3-ts@2.0.2: + resolution: {integrity: sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==} + dependencies: + yaml: 1.10.2 + dev: true + + /optionator@0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: true + + /os-locale@5.0.0: + resolution: {integrity: sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==} + engines: {node: '>=10'} + dependencies: + execa: 4.1.0 + lcid: 3.1.1 + mem: 5.1.1 + dev: true + + /p-defer@1.0.0: + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} + dev: true + + /p-event@4.2.0: + resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} + engines: {node: '>=8'} + dependencies: + p-timeout: 3.2.0 + dev: true + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: true + + /p-is-promise@2.1.0: + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + dependencies: + dot-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.12.11 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: true + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /path-case@3.0.4: + resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-scurry@1.6.1: + resolution: {integrity: sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==} + engines: {node: '>=14'} + dependencies: + lru-cache: 7.18.1 + minipass: 4.2.4 + dev: true + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: true + + /path-to-regexp@1.8.0: + resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} + dependencies: + isarray: 0.0.1 + dev: true + + /path-to-regexp@6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pathe@1.1.0: + resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pino-abstract-transport@1.0.0: + resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==} + dependencies: + readable-stream: 4.3.0 + split2: 4.1.0 + dev: true + + /pino-std-serializers@6.1.0: + resolution: {integrity: sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==} + dev: true + + /pino@8.11.0: + resolution: {integrity: sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.1.2 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pino-std-serializers: 6.1.0 + process-warning: 2.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.2 + sonic-boom: 3.2.1 + thread-stream: 2.3.0 + dev: true + + /pirates@4.0.5: + resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} + engines: {node: '>= 6'} + dev: true + + /pkg-types@1.0.2: + resolution: {integrity: sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.1.1 + pathe: 1.1.0 + dev: true + + /pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + + /postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + dev: true + + /postcss-selector-parser@6.0.11: + resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss@8.4.14: + resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /postcss@8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + + /pretty-quick@3.1.3(prettier@2.8.8): + resolution: {integrity: sha512-kOCi2FJabvuh1as9enxYmrnBC6tVMoVOenMaBqRfsvBHB0cbpYHjdQEpSglpASDFEXVwplpcGR4CLEaisYAFcA==} + engines: {node: '>=10.13'} + hasBin: true + peerDependencies: + prettier: '>=2.0.0' + dependencies: + chalk: 3.0.0 + execa: 4.1.0 + find-up: 4.1.0 + ignore: 5.2.4 + mri: 1.2.0 + multimatch: 4.0.0 + prettier: 2.8.8 + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /process-warning@2.1.0: + resolution: {integrity: sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg==} + dev: true + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: true + + /propagate@2.0.1: + resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} + engines: {node: '>= 8'} + dev: true + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: true + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + dev: false + + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: false + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + + /querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + dev: true + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: true + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: true + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: true + + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: true + + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /readable-stream@4.3.0: + resolution: {integrity: sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + dev: true + + /readdirp@3.5.0: + resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: true + + /reflect-metadata@0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + dev: true + + /regexp-tree@0.1.24: + resolution: {integrity: sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==} + hasBin: true + dev: true + + /regexp.prototype.flags@1.4.3: + resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + dev: true + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve@1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.0 + dev: true + + /rollup@3.18.0: + resolution: {integrity: sha512-J8C6VfEBjkvYPESMQYxKHxNOh4A5a3FlP+0BETGo34HEcE4eTlgCrO2+eWzlu2a/sHs2QUkZco+wscH7jhhgWg==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-regex: 1.1.4 + dev: true + + /safe-regex2@2.0.0: + resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} + dependencies: + ret: 0.2.2 + dev: true + + /safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + dependencies: + regexp-tree: 0.1.24 + dev: true + + /safe-stable-stringify@2.4.2: + resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==} + engines: {node: '>=10'} + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + /sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: true + + /scmp@2.1.0: + resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} + dev: false + + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: true + + /semver@5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + + /semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: true + + /semver@7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /sentence-case@3.0.4: + resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + upper-case-first: 2.0.2 + dev: true + + /serialize-javascript@6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: true + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: true + + /set-cookie-parser@2.5.1: + resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} + dev: true + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /shiki@0.14.1: + resolution: {integrity: sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw==} + dependencies: + ansi-sequence-parser: 1.1.0 + jsonc-parser: 3.2.0 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + object-inspect: 1.12.3 + + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /sinon@14.0.2: + resolution: {integrity: sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 9.1.2 + '@sinonjs/samsam': 7.0.1 + diff: 5.1.0 + nise: 5.1.4 + supports-color: 7.2.0 + dev: true + + /sinon@15.0.1: + resolution: {integrity: sha512-PZXKc08f/wcA/BMRGBze2Wmw50CWPiAH3E21EOi4B49vJ616vW4DQh4fQrqsYox2aNR/N3kCqLuB0PwwOucQrg==} + dependencies: + '@sinonjs/commons': 2.0.0 + '@sinonjs/fake-timers': 10.0.2 + '@sinonjs/samsam': 7.0.1 + diff: 5.1.0 + nise: 5.1.4 + supports-color: 7.2.0 + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + dev: true + + /snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /sonic-boom@3.2.1: + resolution: {integrity: sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A==} + dependencies: + atomic-sleep: 1.0.0 + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: true + + /spdx-correct@3.1.1: + resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.12 + dev: true + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.12 + dev: true + + /spdx-license-ids@3.0.12: + resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} + dev: true + + /split2@4.1.0: + resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==} + engines: {node: '>= 10.x'} + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + dev: true + + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + dev: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + /std-env@3.3.2: + resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} + dev: true + + /stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: true + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + dev: true + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.1 + dev: true + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /strip-literal@1.0.1: + resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + dependencies: + acorn: 8.8.2 + dev: true + + /strong-error-handler@4.0.1: + resolution: {integrity: sha512-wGqTVKwyngu9fjKBCqRuBOooCsHqs4q4AEz9Kk+yMNf+fEjEKf4E6dWw+IT3Y0LxPIdrnu0IE4S5Et97veMXMw==} + engines: {node: '>=10'} + dependencies: + accepts: 1.3.8 + debug: 4.3.4(supports-color@8.1.1) + ejs: 3.1.8 + fast-safe-stringify: 2.1.1 + http-status: 1.6.2 + js2xmlparser: 4.0.2 + strong-globalize: 6.0.5 + transitivePeerDependencies: + - supports-color + dev: true + + /strong-globalize@6.0.5: + resolution: {integrity: sha512-7nfUli41TieV9/TSc0N62ve5Q4nfrpy/T0nNNy6TyD3vst79QWmeylCyd3q1gDxh8dqGEtabLNCdPQP1Iuvecw==} + engines: {node: '>=10'} + dependencies: + accept-language: 3.0.18 + debug: 4.3.4(supports-color@8.1.1) + globalize: 1.7.0 + lodash: 4.17.21 + md5: 2.3.0 + mkdirp: 1.0.4 + os-locale: 5.0.0 + yamljs: 0.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /styled-jsx@5.1.1(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.2.0 + dev: true + + /sucrase@3.29.0: + resolution: {integrity: sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A==} + engines: {node: '>=8'} + hasBin: true + dependencies: + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.5 + ts-interface-checker: 0.1.13 + dev: true + + /superagent@8.0.9: + resolution: {integrity: sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==} + engines: {node: '>=6.4.0 <13 || >=14'} + dependencies: + component-emitter: 1.3.0 + cookiejar: 2.1.4 + debug: 4.3.4(supports-color@8.1.1) + fast-safe-stringify: 2.1.1 + form-data: 4.0.0 + formidable: 2.1.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.11.0 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + dev: true + + /supertest@6.3.3: + resolution: {integrity: sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==} + engines: {node: '>=6.4.0'} + dependencies: + methods: 1.1.2 + superagent: 8.0.9 + transitivePeerDependencies: + - supports-color + dev: true + + /supertokens-js-override@0.0.4: + resolution: {integrity: sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==} + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /thread-stream@2.3.0: + resolution: {integrity: sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==} + dependencies: + real-require: 0.2.0 + dev: true + + /tiny-lru@10.0.1: + resolution: {integrity: sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA==} + engines: {node: '>=6'} + dev: true + + /tinybench@2.4.0: + resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==} + dev: true + + /tinypool@0.3.1: + resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@1.1.1: + resolution: {integrity: sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==} + engines: {node: '>=14.0.0'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + /toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: true + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.0 + dev: true + + /traverse@0.6.7: + resolution: {integrity: sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==} + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + dev: true + + /tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + dev: true + + /tsup@6.6.3(typescript@4.2.4): + resolution: {integrity: sha512-OLx/jFllYlVeZQ7sCHBuRVEQBBa1tFbouoc/gbYakyipjVQdWy/iQOvmExUA/ewap9iQ7tbJf9pW0PgcEFfJcQ==} + engines: {node: '>=14.18'} + hasBin: true + peerDependencies: + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: ^4.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 4.0.1(esbuild@0.17.11) + cac: 6.7.14 + chokidar: 3.5.1 + debug: 4.3.4(supports-color@8.1.1) + esbuild: 0.17.11 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 3.1.4 + resolve-from: 5.0.0 + rollup: 3.18.0 + source-map: 0.8.0-beta.0 + sucrase: 3.29.0 + tree-kill: 1.2.2 + typescript: 4.2.4 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + + /tsutils@3.21.0(typescript@4.2.4): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.2.4 + dev: true + + /twilio@4.8.0(debug@4.3.4): + resolution: {integrity: sha512-jJaEyFGIiIAIfAWyq94g3uo2odTyo2opRN8hzpDHpbA4SYDfhxmm4E+Z0c7AP41HEdxzDyCwMkLNXh6fBpWRiw==} + engines: {node: '>=14.0'} + dependencies: + axios: 0.26.1(debug@4.3.4) + dayjs: 1.11.7 + https-proxy-agent: 5.0.1 + jsonwebtoken: 9.0.0 + qs: 6.11.0 + scmp: 2.1.0 + url-parse: 1.5.10 + xmlbuilder: 13.0.2 + transitivePeerDependencies: + - debug + - supports-color + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + dev: true + + /typedoc@0.23.26(typescript@4.2.4): + resolution: {integrity: sha512-5m4KwR5tOLnk0OtMaRn9IdbeRM32uPemN9kur7YK9wFqx8U0CYrvO9aVq6ysdZSV1c824BTm+BuQl2Ze/k1HtA==} + engines: {node: '>= 14.14'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x + dependencies: + lunr: 2.3.9 + marked: 4.2.12 + minimatch: 7.4.2 + shiki: 0.14.1 + typescript: 4.2.4 + dev: true + + /typescript@4.2.4: + resolution: {integrity: sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /ufo@1.1.1: + resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + /upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + dependencies: + tslib: 2.5.0 + dev: true + + /upper-case@2.0.2: + resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + dependencies: + tslib: 2.5.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + + /url@0.10.3: + resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.10 + which-typed-array: 1.1.9 + dev: true + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: true + + /uuid-parse@1.1.0: + resolution: {integrity: sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==} + dev: true + + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: true + + /uuid@8.0.0: + resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + hasBin: true + dev: true + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: true + + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: true + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.1.1 + spdx-expression-parse: 3.0.1 + dev: true + + /validator@13.9.0: + resolution: {integrity: sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==} + engines: {node: '>= 0.10'} + dev: true + + /vandium-utils@1.2.0: + resolution: {integrity: sha512-yxYUDZz4BNo0CW/z5w4mvclitt5zolY7zjW97i6tTE+sU63cxYs1A6Bl9+jtIQa3+0hkeqY87k+7ptRvmeHe3g==} + dev: true + + /vandium-utils@2.0.0: + resolution: {integrity: sha512-XWbQ/0H03TpYDXk8sLScBEZpE7TbA0CHDL6/Xjt37IBYKLsHUQuBlL44ttAUs9zoBOLFxsW7HehXcuWCNyqOxQ==} + engines: {node: '>=10.16'} + dev: true + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: true + + /verify-apple-id-token@3.0.1: + resolution: {integrity: sha512-q91pG1e52TpEzXldMirWYNWcSQC4WuzgG0y/ZnBhzjfk0pSxi4YlGh5OTVRlodBenayGHfSDn5VseG9QDuqOew==} + dependencies: + jsonwebtoken: 9.0.0 + jwks-rsa: 3.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /vite-node@0.29.2(@types/node@18.15.9): + resolution: {integrity: sha512-5oe1z6wzI3gkvc4yOBbDBbgpiWiApvuN4P55E8OI131JGrSuo4X3SOZrNmZYo4R8Zkze/dhi572blX0zc+6SdA==} + engines: {node: '>=v14.16.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4(supports-color@8.1.1) + mlly: 1.1.1 + pathe: 1.1.0 + picocolors: 1.0.0 + vite: 4.1.4(@types/node@18.15.9) + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite@4.1.4(@types/node@18.15.9): + resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.15.9 + esbuild: 0.16.17 + postcss: 8.4.21 + resolve: 1.22.1 + rollup: 3.18.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitest@0.29.2: + resolution: {integrity: sha512-ydK9IGbAvoY8wkg29DQ4ivcVviCaUi3ivuPKfZEVddMTenFHUfB8EEDXQV8+RasEk1ACFLgMUqAaDuQ/Nk+mQA==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.4 + '@types/chai-subset': 1.3.3 + '@types/node': 18.15.9 + '@vitest/expect': 0.29.2 + '@vitest/runner': 0.29.2 + '@vitest/spy': 0.29.2 + '@vitest/utils': 0.29.2 + acorn: 8.8.2 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + debug: 4.3.4(supports-color@8.1.1) + local-pkg: 0.4.3 + pathe: 1.1.0 + picocolors: 1.0.0 + source-map: 0.6.1 + std-env: 3.3.2 + strip-literal: 1.0.1 + tinybench: 2.4.0 + tinypool: 0.3.1 + tinyspy: 1.1.1 + vite: 4.1.4(@types/node@18.15.9) + vite-node: 0.29.2(@types/node@18.15.9) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vscode-oniguruma@1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: true + + /vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + dev: true + + /vue-eslint-parser@9.1.0(eslint@8.35.0): + resolution: {integrity: sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.35.0 + eslint-scope: 7.1.1 + eslint-visitor-keys: 3.3.0 + espree: 9.4.1 + esquery: 1.5.0 + lodash: 4.17.21 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: true + + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /word-wrap@1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + + /workerpool@6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xml2js@0.4.19: + resolution: {integrity: sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==} + dependencies: + sax: 1.2.1 + xmlbuilder: 9.0.7 + dev: true + + /xmlbuilder@13.0.2: + resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==} + engines: {node: '>=6.0'} + dev: false + + /xmlbuilder@9.0.7: + resolution: {integrity: sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==} + engines: {node: '>=4.0'} + dev: true + + /xmlcreate@2.0.4: + resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml-eslint-parser@1.1.0: + resolution: {integrity: sha512-b464Q1fYiX1oYx2kE8k4mEp6S9Prk+tfDsY/IPxQ0FCjEuj3AKko5Skf3/yQJeYTTDyjDE+aWIJemnv29HvEWQ==} + engines: {node: ^14.17.0 || >=16.0.0} + dependencies: + eslint-visitor-keys: 3.3.0 + lodash: 4.17.21 + yaml: 2.2.1 + dev: true + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + + /yaml@2.2.1: + resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==} + engines: {node: '>= 14'} + dev: true + + /yamljs@0.3.0: + resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==} + hasBin: true + dependencies: + argparse: 1.0.10 + glob: 7.2.0 + dev: true + + /yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: true + + /yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + dev: true + + /ylru@1.3.2: + resolution: {integrity: sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==} + engines: {node: '>= 4.0.0'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000..425e4d962 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - playground \ No newline at end of file diff --git a/recipe/dashboard/index.d.ts b/recipe/dashboard/index.d.ts deleted file mode 100644 index 6d1776ce3..000000000 --- a/recipe/dashboard/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/dashboard"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/dashboard"; -export default _default; diff --git a/recipe/dashboard/index.js b/recipe/dashboard/index.js deleted file mode 100644 index fc697e4c9..000000000 --- a/recipe/dashboard/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/dashboard")); diff --git a/recipe/dashboard/types/index.d.ts b/recipe/dashboard/types/index.d.ts deleted file mode 100644 index 23332774e..000000000 --- a/recipe/dashboard/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/dashboard/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/dashboard/types"; -export default _default; diff --git a/recipe/dashboard/types/index.js b/recipe/dashboard/types/index.js deleted file mode 100644 index c797bdb53..000000000 --- a/recipe/dashboard/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/dashboard/types")); diff --git a/recipe/emailpassword/emaildelivery/index.d.ts b/recipe/emailpassword/emaildelivery/index.d.ts deleted file mode 100644 index dd7c2a8ea..000000000 --- a/recipe/emailpassword/emaildelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/emailpassword/emaildelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/emailpassword/emaildelivery/services"; -export default _default; diff --git a/recipe/emailpassword/emaildelivery/index.js b/recipe/emailpassword/emaildelivery/index.js deleted file mode 100644 index 894662752..000000000 --- a/recipe/emailpassword/emaildelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/emailpassword/emaildelivery/services")); diff --git a/recipe/emailpassword/index.d.ts b/recipe/emailpassword/index.d.ts deleted file mode 100644 index 84304e441..000000000 --- a/recipe/emailpassword/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/emailpassword"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/emailpassword"; -export default _default; diff --git a/recipe/emailpassword/index.js b/recipe/emailpassword/index.js deleted file mode 100644 index a84b0c3fc..000000000 --- a/recipe/emailpassword/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/emailpassword")); diff --git a/recipe/emailpassword/types/index.d.ts b/recipe/emailpassword/types/index.d.ts deleted file mode 100644 index 7ff35cd6e..000000000 --- a/recipe/emailpassword/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/emailpassword/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/emailpassword/types"; -export default _default; diff --git a/recipe/emailpassword/types/index.js b/recipe/emailpassword/types/index.js deleted file mode 100644 index 1d1844085..000000000 --- a/recipe/emailpassword/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/emailpassword/types")); diff --git a/recipe/emailverification/emaildelivery/index.d.ts b/recipe/emailverification/emaildelivery/index.d.ts deleted file mode 100644 index e342f96ab..000000000 --- a/recipe/emailverification/emaildelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/emailverification/emaildelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/emailverification/emaildelivery/services"; -export default _default; diff --git a/recipe/emailverification/emaildelivery/index.js b/recipe/emailverification/emaildelivery/index.js deleted file mode 100644 index f7f1bd525..000000000 --- a/recipe/emailverification/emaildelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/emailverification/emaildelivery/services")); diff --git a/recipe/emailverification/index.d.ts b/recipe/emailverification/index.d.ts deleted file mode 100644 index 525d2c2c5..000000000 --- a/recipe/emailverification/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/emailverification"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/emailverification"; -export default _default; diff --git a/recipe/emailverification/index.js b/recipe/emailverification/index.js deleted file mode 100644 index 6c69e3702..000000000 --- a/recipe/emailverification/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/emailverification")); diff --git a/recipe/emailverification/types/index.d.ts b/recipe/emailverification/types/index.d.ts deleted file mode 100644 index 60dd4a01d..000000000 --- a/recipe/emailverification/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/emailverification/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/emailverification/types"; -export default _default; diff --git a/recipe/emailverification/types/index.js b/recipe/emailverification/types/index.js deleted file mode 100644 index d028dad88..000000000 --- a/recipe/emailverification/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/emailverification/types")); diff --git a/recipe/jwt/index.d.ts b/recipe/jwt/index.d.ts deleted file mode 100644 index 5ddb6bfc1..000000000 --- a/recipe/jwt/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/jwt"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/jwt"; -export default _default; diff --git a/recipe/jwt/index.js b/recipe/jwt/index.js deleted file mode 100644 index 31f0b5258..000000000 --- a/recipe/jwt/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/jwt")); diff --git a/recipe/jwt/types/index.d.ts b/recipe/jwt/types/index.d.ts deleted file mode 100644 index e917e36d3..000000000 --- a/recipe/jwt/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/jwt/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/jwt/types"; -export default _default; diff --git a/recipe/jwt/types/index.js b/recipe/jwt/types/index.js deleted file mode 100644 index ba35c27c4..000000000 --- a/recipe/jwt/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/jwt/types")); diff --git a/recipe/passwordless/emaildelivery/index.d.ts b/recipe/passwordless/emaildelivery/index.d.ts deleted file mode 100644 index cad39adda..000000000 --- a/recipe/passwordless/emaildelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/passwordless/emaildelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/passwordless/emaildelivery/services"; -export default _default; diff --git a/recipe/passwordless/emaildelivery/index.js b/recipe/passwordless/emaildelivery/index.js deleted file mode 100644 index f43219743..000000000 --- a/recipe/passwordless/emaildelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/passwordless/emaildelivery/services")); diff --git a/recipe/passwordless/index.d.ts b/recipe/passwordless/index.d.ts deleted file mode 100644 index dea3a5689..000000000 --- a/recipe/passwordless/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/passwordless"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/passwordless"; -export default _default; diff --git a/recipe/passwordless/index.js b/recipe/passwordless/index.js deleted file mode 100644 index 83bdd4ca7..000000000 --- a/recipe/passwordless/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/passwordless")); diff --git a/recipe/passwordless/smsdelivery/index.d.ts b/recipe/passwordless/smsdelivery/index.d.ts deleted file mode 100644 index 858854cb6..000000000 --- a/recipe/passwordless/smsdelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/passwordless/smsdelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/passwordless/smsdelivery/services"; -export default _default; diff --git a/recipe/passwordless/smsdelivery/index.js b/recipe/passwordless/smsdelivery/index.js deleted file mode 100644 index 1fbd9a3e0..000000000 --- a/recipe/passwordless/smsdelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/passwordless/smsdelivery/services")); diff --git a/recipe/passwordless/types/index.d.ts b/recipe/passwordless/types/index.d.ts deleted file mode 100644 index a7b9e5020..000000000 --- a/recipe/passwordless/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/passwordless/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/passwordless/types"; -export default _default; diff --git a/recipe/passwordless/types/index.js b/recipe/passwordless/types/index.js deleted file mode 100644 index f7eaf1795..000000000 --- a/recipe/passwordless/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/passwordless/types")); diff --git a/recipe/session/claims.d.ts b/recipe/session/claims.d.ts deleted file mode 100644 index 2a0641c72..000000000 --- a/recipe/session/claims.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/session/claims"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/session/claims"; -export default _default; diff --git a/recipe/session/claims.js b/recipe/session/claims.js deleted file mode 100644 index 31014014a..000000000 --- a/recipe/session/claims.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/session/claims")); diff --git a/recipe/session/framework/awsLambda/index.d.ts b/recipe/session/framework/awsLambda/index.d.ts deleted file mode 100644 index 9fffcbee6..000000000 --- a/recipe/session/framework/awsLambda/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../../../lib/build/recipe/session/framework/awsLambda"; -import * as _default from "../../../../lib/build/recipe/session/framework/awsLambda"; -export default _default; diff --git a/recipe/session/framework/awsLambda/index.js b/recipe/session/framework/awsLambda/index.js deleted file mode 100644 index 47a35f11a..000000000 --- a/recipe/session/framework/awsLambda/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../../lib/build/recipe/session/framework/awsLambda")); diff --git a/recipe/session/framework/express/index.d.ts b/recipe/session/framework/express/index.d.ts deleted file mode 100644 index 1326741fc..000000000 --- a/recipe/session/framework/express/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../../../lib/build/recipe/session/framework/express"; -import * as _default from "../../../../lib/build/recipe/session/framework/express"; -export default _default; diff --git a/recipe/session/framework/express/index.js b/recipe/session/framework/express/index.js deleted file mode 100644 index da2b16185..000000000 --- a/recipe/session/framework/express/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../../lib/build/recipe/session/framework/express")); diff --git a/recipe/session/framework/fastify/index.d.ts b/recipe/session/framework/fastify/index.d.ts deleted file mode 100644 index 20535b62c..000000000 --- a/recipe/session/framework/fastify/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../../../lib/build/recipe/session/framework/fastify"; -import * as _default from "../../../../lib/build/recipe/session/framework/fastify"; -export default _default; diff --git a/recipe/session/framework/fastify/index.js b/recipe/session/framework/fastify/index.js deleted file mode 100644 index 4f0373f8c..000000000 --- a/recipe/session/framework/fastify/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../../lib/build/recipe/session/framework/fastify")); diff --git a/recipe/session/framework/hapi/index.d.ts b/recipe/session/framework/hapi/index.d.ts deleted file mode 100644 index 2365a29c2..000000000 --- a/recipe/session/framework/hapi/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../../../lib/build/recipe/session/framework/hapi"; -import * as _default from "../../../../lib/build/recipe/session/framework/hapi"; -export default _default; diff --git a/recipe/session/framework/hapi/index.js b/recipe/session/framework/hapi/index.js deleted file mode 100644 index 21babe7b9..000000000 --- a/recipe/session/framework/hapi/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../../lib/build/recipe/session/framework/hapi")); diff --git a/recipe/session/framework/koa/index.d.ts b/recipe/session/framework/koa/index.d.ts deleted file mode 100644 index fe57a8be2..000000000 --- a/recipe/session/framework/koa/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../../../lib/build/recipe/session/framework/koa"; -import * as _default from "../../../../lib/build/recipe/session/framework/koa"; -export default _default; diff --git a/recipe/session/framework/koa/index.js b/recipe/session/framework/koa/index.js deleted file mode 100644 index e94cef8f0..000000000 --- a/recipe/session/framework/koa/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../../lib/build/recipe/session/framework/koa")); diff --git a/recipe/session/framework/loopback/index.d.ts b/recipe/session/framework/loopback/index.d.ts deleted file mode 100644 index 01587b8fd..000000000 --- a/recipe/session/framework/loopback/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "../../../../lib/build/recipe/session/framework/loopback"; -import * as _default from "../../../../lib/build/recipe/session/framework/loopback"; -export default _default; diff --git a/recipe/session/framework/loopback/index.js b/recipe/session/framework/loopback/index.js deleted file mode 100644 index 67c8a2913..000000000 --- a/recipe/session/framework/loopback/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../../lib/build/recipe/session/framework/loopback")); diff --git a/recipe/session/index.d.ts b/recipe/session/index.d.ts deleted file mode 100644 index 6e092fdfc..000000000 --- a/recipe/session/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/session"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/session"; -export default _default; diff --git a/recipe/session/index.js b/recipe/session/index.js deleted file mode 100644 index 39159f318..000000000 --- a/recipe/session/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/session")); diff --git a/recipe/session/types/index.d.ts b/recipe/session/types/index.d.ts deleted file mode 100644 index 227152642..000000000 --- a/recipe/session/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/session/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/session/types"; -export default _default; diff --git a/recipe/session/types/index.js b/recipe/session/types/index.js deleted file mode 100644 index 0368ce9cb..000000000 --- a/recipe/session/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/session/types")); diff --git a/recipe/thirdparty/emaildelivery/index.d.ts b/recipe/thirdparty/emaildelivery/index.d.ts deleted file mode 100644 index 30bed2519..000000000 --- a/recipe/thirdparty/emaildelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/thirdparty/emaildelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/thirdparty/emaildelivery/services"; -export default _default; diff --git a/recipe/thirdparty/emaildelivery/index.js b/recipe/thirdparty/emaildelivery/index.js deleted file mode 100644 index e0c46c07f..000000000 --- a/recipe/thirdparty/emaildelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/thirdparty/emaildelivery/services")); diff --git a/recipe/thirdparty/index.d.ts b/recipe/thirdparty/index.d.ts deleted file mode 100644 index bac464cc1..000000000 --- a/recipe/thirdparty/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/thirdparty"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/thirdparty"; -export default _default; diff --git a/recipe/thirdparty/index.js b/recipe/thirdparty/index.js deleted file mode 100644 index 6c3ee939d..000000000 --- a/recipe/thirdparty/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/thirdparty")); diff --git a/recipe/thirdparty/types/index.d.ts b/recipe/thirdparty/types/index.d.ts deleted file mode 100644 index 90cd6d381..000000000 --- a/recipe/thirdparty/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/thirdparty/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/thirdparty/types"; -export default _default; diff --git a/recipe/thirdparty/types/index.js b/recipe/thirdparty/types/index.js deleted file mode 100644 index e31ef1957..000000000 --- a/recipe/thirdparty/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/thirdparty/types")); diff --git a/recipe/thirdpartyemailpassword/emaildelivery/index.d.ts b/recipe/thirdpartyemailpassword/emaildelivery/index.d.ts deleted file mode 100644 index cf01c2f41..000000000 --- a/recipe/thirdpartyemailpassword/emaildelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/thirdpartyemailpassword/emaildelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/thirdpartyemailpassword/emaildelivery/services"; -export default _default; diff --git a/recipe/thirdpartyemailpassword/emaildelivery/index.js b/recipe/thirdpartyemailpassword/emaildelivery/index.js deleted file mode 100644 index e82810ec0..000000000 --- a/recipe/thirdpartyemailpassword/emaildelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/thirdpartyemailpassword/emaildelivery/services")); diff --git a/recipe/thirdpartyemailpassword/index.d.ts b/recipe/thirdpartyemailpassword/index.d.ts deleted file mode 100644 index 4d9831457..000000000 --- a/recipe/thirdpartyemailpassword/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/thirdpartyemailpassword"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/thirdpartyemailpassword"; -export default _default; diff --git a/recipe/thirdpartyemailpassword/index.js b/recipe/thirdpartyemailpassword/index.js deleted file mode 100644 index 69ed58033..000000000 --- a/recipe/thirdpartyemailpassword/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/thirdpartyemailpassword")); diff --git a/recipe/thirdpartyemailpassword/types/index.d.ts b/recipe/thirdpartyemailpassword/types/index.d.ts deleted file mode 100644 index 2b42327ca..000000000 --- a/recipe/thirdpartyemailpassword/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/thirdpartyemailpassword/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/thirdpartyemailpassword/types"; -export default _default; diff --git a/recipe/thirdpartyemailpassword/types/index.js b/recipe/thirdpartyemailpassword/types/index.js deleted file mode 100644 index 27529f7b6..000000000 --- a/recipe/thirdpartyemailpassword/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/thirdpartyemailpassword/types")); diff --git a/recipe/thirdpartypasswordless/emaildelivery/index.d.ts b/recipe/thirdpartypasswordless/emaildelivery/index.d.ts deleted file mode 100644 index 870756309..000000000 --- a/recipe/thirdpartypasswordless/emaildelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/thirdpartypasswordless/emaildelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/thirdpartypasswordless/emaildelivery/services"; -export default _default; diff --git a/recipe/thirdpartypasswordless/emaildelivery/index.js b/recipe/thirdpartypasswordless/emaildelivery/index.js deleted file mode 100644 index 1084a585d..000000000 --- a/recipe/thirdpartypasswordless/emaildelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/thirdpartypasswordless/emaildelivery/services")); diff --git a/recipe/thirdpartypasswordless/index.d.ts b/recipe/thirdpartypasswordless/index.d.ts deleted file mode 100644 index 39bc7056d..000000000 --- a/recipe/thirdpartypasswordless/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/thirdpartypasswordless"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/thirdpartypasswordless"; -export default _default; diff --git a/recipe/thirdpartypasswordless/index.js b/recipe/thirdpartypasswordless/index.js deleted file mode 100644 index d3a7c9df7..000000000 --- a/recipe/thirdpartypasswordless/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/thirdpartypasswordless")); diff --git a/recipe/thirdpartypasswordless/smsdelivery/index.d.ts b/recipe/thirdpartypasswordless/smsdelivery/index.d.ts deleted file mode 100644 index 8998e9d76..000000000 --- a/recipe/thirdpartypasswordless/smsdelivery/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/thirdpartypasswordless/smsdelivery/services"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/thirdpartypasswordless/smsdelivery/services"; -export default _default; diff --git a/recipe/thirdpartypasswordless/smsdelivery/index.js b/recipe/thirdpartypasswordless/smsdelivery/index.js deleted file mode 100644 index 19f6ba8c1..000000000 --- a/recipe/thirdpartypasswordless/smsdelivery/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/thirdpartypasswordless/smsdelivery/services")); diff --git a/recipe/thirdpartypasswordless/types/index.d.ts b/recipe/thirdpartypasswordless/types/index.d.ts deleted file mode 100644 index ba9c55e93..000000000 --- a/recipe/thirdpartypasswordless/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/thirdpartypasswordless/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/thirdpartypasswordless/types"; -export default _default; diff --git a/recipe/thirdpartypasswordless/types/index.js b/recipe/thirdpartypasswordless/types/index.js deleted file mode 100644 index 5d90dc78c..000000000 --- a/recipe/thirdpartypasswordless/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/thirdpartypasswordless/types")); diff --git a/recipe/usermetadata/index.d.ts b/recipe/usermetadata/index.d.ts deleted file mode 100644 index e541f61b4..000000000 --- a/recipe/usermetadata/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/usermetadata"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/usermetadata"; -export default _default; diff --git a/recipe/usermetadata/index.js b/recipe/usermetadata/index.js deleted file mode 100644 index a987bf05f..000000000 --- a/recipe/usermetadata/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/usermetadata")); diff --git a/recipe/usermetadata/types/index.d.ts b/recipe/usermetadata/types/index.d.ts deleted file mode 100644 index cd5315b15..000000000 --- a/recipe/usermetadata/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/usermetadata/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/usermetadata/types"; -export default _default; diff --git a/recipe/usermetadata/types/index.js b/recipe/usermetadata/types/index.js deleted file mode 100644 index cbd50db7d..000000000 --- a/recipe/usermetadata/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/usermetadata/types")); diff --git a/recipe/userroles/index.d.ts b/recipe/userroles/index.d.ts deleted file mode 100644 index 890df5725..000000000 --- a/recipe/userroles/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../lib/build/recipe/userroles"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../lib/build/recipe/userroles"; -export default _default; diff --git a/recipe/userroles/index.js b/recipe/userroles/index.js deleted file mode 100644 index 5f41d3af9..000000000 --- a/recipe/userroles/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../lib/build/recipe/userroles")); diff --git a/recipe/userroles/types/index.d.ts b/recipe/userroles/types/index.d.ts deleted file mode 100644 index 424c2c2db..000000000 --- a/recipe/userroles/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../../../lib/build/recipe/userroles/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../../../lib/build/recipe/userroles/types"; -export default _default; diff --git a/recipe/userroles/types/index.js b/recipe/userroles/types/index.js deleted file mode 100644 index 9b42a3278..000000000 --- a/recipe/userroles/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../../../lib/build/recipe/userroles/types")); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..28a1a265c --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,17 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const HEADER_RID = 'rid' +export const HEADER_FDI = 'fdi-version' diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 000000000..b589523bc --- /dev/null +++ b/src/error.ts @@ -0,0 +1,55 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export default class SuperTokensError extends Error { + private static errMagic = 'ndskajfasndlfkj435234krjdsa' + static BAD_INPUT_ERROR = 'BAD_INPUT_ERROR' as const + + public type: string + public payload: any + + // this variable is used to identify which + // recipe initiated this error. If no recipe + // initiated it, it will be undefined, else it + // will be the "actual" rid of that recipe. By actual, + // I mean that it will not be influenced by the + // parent's RID. + public fromRecipe: string | undefined + + private errMagic: string + + constructor( + options: + | { + message: string + payload?: any + type: string + } + | { + message: string + type: 'BAD_INPUT_ERROR' + payload: undefined + }, + ) { + super(options.message) + this.type = options.type + this.payload = options.payload + this.errMagic = SuperTokensError.errMagic + } + + static isErrorFromSuperTokens(obj: any): obj is SuperTokensError { + return obj.errMagic === SuperTokensError.errMagic + } +} diff --git a/src/framework/awsLambda/framework.ts b/src/framework/awsLambda/framework.ts new file mode 100644 index 000000000..cecc489c1 --- /dev/null +++ b/src/framework/awsLambda/framework.ts @@ -0,0 +1,371 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { parse } from 'querystring' +import { URL } from 'url' +import type { + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + APIGatewayProxyResult, + APIGatewayProxyStructuredResultV2, + Callback, + Context, + Handler, +} from 'aws-lambda' +import { HTTPMethod } from '../../types' +import { getFromObjectCaseInsensitive, normaliseHttpMethod } from '../../utils' +import { BaseRequest } from '../request' +import { BaseResponse } from '../response' +import { getCookieValueFromHeaders, normalizeHeaderValue, serializeCookieValue } from '../utils' +import { COOKIE_HEADER } from '../constants' +import { SessionContainerInterface } from '../../recipe/session/types' +import SuperTokens from '../../supertokens' +import { Framework } from '../types' + +export class AWSRequest extends BaseRequest { + private event: APIGatewayProxyEventV2 | APIGatewayProxyEvent + private parsedJSONBody: Object | undefined + private parsedUrlEncodedFormData: Object | undefined + + constructor(event: APIGatewayProxyEventV2 | APIGatewayProxyEvent) { + super() + this.original = event + this.event = event + this.parsedJSONBody = undefined + this.parsedUrlEncodedFormData = undefined + } + + getFormData = async (): Promise => { + if (this.parsedUrlEncodedFormData === undefined) { + if (this.event.body === null || this.event.body === undefined) { + this.parsedUrlEncodedFormData = {} + } + else { + this.parsedUrlEncodedFormData = parse(this.event.body) + if (this.parsedUrlEncodedFormData === undefined) + this.parsedUrlEncodedFormData = {} + } + } + return this.parsedUrlEncodedFormData + } + + getKeyValueFromQuery = (key: string): string | undefined => { + if (this.event.queryStringParameters === undefined || this.event.queryStringParameters === null) + return undefined + + const value = this.event.queryStringParameters[key] + if (value === undefined || typeof value !== 'string') + return undefined + + return value + } + + getJSONBody = async (): Promise => { + if (this.parsedJSONBody === undefined) { + if (this.event.body === null || this.event.body === undefined) { + this.parsedJSONBody = {} + } + else { + this.parsedJSONBody = JSON.parse(this.event.body) + if (this.parsedJSONBody === undefined) + this.parsedJSONBody = {} + } + } + return this.parsedJSONBody + } + + getMethod = (): HTTPMethod => { + const rawMethod = (this.event as APIGatewayProxyEvent).httpMethod + if (rawMethod !== undefined) + return normaliseHttpMethod(rawMethod) + + return normaliseHttpMethod((this.event as APIGatewayProxyEventV2).requestContext.http.method) + } + + getCookieValue = (key: string): string | undefined => { + const cookies = (this.event as APIGatewayProxyEventV2).cookies + if ( + (this.event.headers === undefined || this.event.headers === null) + && (cookies === undefined || cookies === null) + ) + return undefined + + let value = getCookieValueFromHeaders(this.event.headers, key) + if (value === undefined && cookies !== undefined && cookies !== null) { + value = getCookieValueFromHeaders( + { + cookie: cookies.join(';'), + }, + key, + ) + } + return value + } + + getHeaderValue = (key: string): string | undefined => { + if (this.event.headers === undefined || this.event.headers === null) + return undefined + + return normalizeHeaderValue(getFromObjectCaseInsensitive(key, this.event.headers)) + } + + getOriginalURL = (): string => { + let path = (this.event as APIGatewayProxyEvent).path + const queryParams = (this.event as APIGatewayProxyEvent).queryStringParameters as { [key: string]: string } + if (path === undefined) { + path = (this.event as APIGatewayProxyEventV2).requestContext.http.path + const stage = (this.event as APIGatewayProxyEventV2).requestContext.stage + if (stage !== undefined && path.startsWith(`/${stage}`)) + path = path.slice(stage.length + 1) + + if (queryParams !== undefined && queryParams !== null) { + const urlString = `https://exmaple.com${path}` + const url = new URL(urlString) + Object.keys(queryParams).forEach(el => url.searchParams.append(el, queryParams[el])) + path = url.pathname + url.search + } + } + return path + } +} + +interface SupertokensLambdaEvent extends APIGatewayProxyEvent { + supertokens: { + response: { + headers: { key: string; value: boolean | number | string; allowDuplicateKey: boolean }[] + cookies: string[] + } + } +} + +interface SupertokensLambdaEventV2 extends APIGatewayProxyEventV2 { + supertokens: { + response: { + headers: { key: string; value: boolean | number | string; allowDuplicateKey: boolean }[] + cookies: string[] + } + } +} + +export class AWSResponse extends BaseResponse { + private statusCode: number + private event: SupertokensLambdaEvent | SupertokensLambdaEventV2 + private content: string + public responseSet: boolean + public statusSet: boolean + + constructor(event: SupertokensLambdaEvent | SupertokensLambdaEventV2) { + super() + this.original = event + this.event = event + this.statusCode = 200 + this.content = JSON.stringify({}) + this.responseSet = false + this.statusSet = false + this.event.supertokens = { + response: { + headers: [], + cookies: [], + }, + } + } + + sendHTMLResponse = (html: string) => { + if (!this.responseSet) { + this.content = html + this.setHeader('Content-Type', 'text/html', false) + this.responseSet = true + } + } + + setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { + this.event.supertokens.response.headers.push({ + key, + value, + allowDuplicateKey, + }) + } + + removeHeader = (key: string) => { + this.event.supertokens.response.headers = this.event.supertokens.response.headers.filter( + header => header.key.toLowerCase() !== key.toLowerCase(), + ) + } + + setCookie = ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', + ) => { + const serialisedCookie = serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite) + this.event.supertokens.response.cookies.push(serialisedCookie) + } + + /** + * @param {number} statusCode + */ + setStatusCode = (statusCode: number) => { + if (!this.statusSet) { + this.statusCode = statusCode + this.statusSet = true + } + } + + sendJSONResponse = (content: any) => { + if (!this.responseSet) { + this.content = JSON.stringify(content) + this.setHeader('Content-Type', 'application/json', false) + this.responseSet = true + } + } + + sendResponse = (response?: APIGatewayProxyResult | APIGatewayProxyStructuredResultV2) => { + if (response === undefined) + response = {} + + let headers: + | { + [header: string]: boolean | number | string + } + | undefined = response.headers + if (headers === undefined) + headers = {} + + let body = response.body + let statusCode = response.statusCode + if (this.responseSet) { + statusCode = this.statusCode + body = this.content + } + const supertokensHeaders = this.event.supertokens.response.headers + const supertokensCookies = this.event.supertokens.response.cookies + for (let i = 0; i < supertokensHeaders.length; i++) { + let currentValue + const currentHeadersSet = Object.keys(headers === undefined ? [] : headers) + for (let j = 0; j < currentHeadersSet.length; j++) { + if (currentHeadersSet[j].toLowerCase() === supertokensHeaders[i].key.toLowerCase()) { + supertokensHeaders[i].key = currentHeadersSet[j] + currentValue = headers[currentHeadersSet[j]] + break + } + } + if (supertokensHeaders[i].allowDuplicateKey && currentValue !== undefined) { + const newValue = `${currentValue}, ${supertokensHeaders[i].value}` + headers[supertokensHeaders[i].key] = newValue + } + else { + headers[supertokensHeaders[i].key] = supertokensHeaders[i].value + } + } + if ((this.event as APIGatewayProxyEventV2).version !== undefined) { + let cookies = (response as APIGatewayProxyStructuredResultV2).cookies + if (cookies === undefined) + cookies = [] + + cookies.push(...supertokensCookies) + + const result: APIGatewayProxyStructuredResultV2 = { + ...(response as APIGatewayProxyStructuredResultV2), + cookies, + body, + statusCode, + headers, + } + return result + } + else { + let multiValueHeaders = (response as APIGatewayProxyResult).multiValueHeaders + if (multiValueHeaders === undefined) + multiValueHeaders = {} + + const headsersInMultiValueHeaders = Object.keys(multiValueHeaders) + const cookieHeader = headsersInMultiValueHeaders.find(h => h.toLowerCase() === COOKIE_HEADER.toLowerCase()) + if (cookieHeader === undefined) + multiValueHeaders[COOKIE_HEADER] = supertokensCookies + + else + multiValueHeaders[cookieHeader].push(...supertokensCookies) + + const result: APIGatewayProxyResult = { + ...(response as APIGatewayProxyResult), + multiValueHeaders, + body: body as string, + statusCode: statusCode as number, + headers, + } + return result + } + } +} + +export interface SessionEventV2 extends SupertokensLambdaEventV2 { + session?: SessionContainerInterface +} + +export interface SessionEvent extends SupertokensLambdaEvent { + session?: SessionContainerInterface +} + +export const middleware = (handler?: Handler): Handler => { + return async (event: SessionEvent | SessionEventV2, context: Context, callback: Callback) => { + const supertokens = SuperTokens.getInstanceOrThrowError() + const request = new AWSRequest(event) + const response = new AWSResponse(event) + try { + const result = await supertokens.middleware(request, response) + if (result) + return response.sendResponse() + + if (handler !== undefined) { + const handlerResult = await handler(event, context, callback) + return response.sendResponse(handlerResult) + } + /** + * it reaches this point only if the API route was not exposed by + * the SDK and user didn't provide a handler + */ + response.setStatusCode(404) + response.sendJSONResponse({ + error: `The middleware couldn't serve the API path ${request.getOriginalURL()}, method: ${request.getMethod()}. If this is an unexpected behaviour, please create an issue here: https://github.com/supertokens/supertokens-node/issues`, + }) + return response.sendResponse() + } + catch (err) { + await supertokens.errorHandler(err, request, response) + if (response.responseSet) + return response.sendResponse() + + throw err + } + } +} + +export interface AWSFramework extends Framework { + middleware: (handler?: Handler) => Handler +} + +export const AWSWrapper: AWSFramework = { + middleware, + wrapRequest: (unwrapped) => { + return new AWSRequest(unwrapped) + }, + wrapResponse: (unwrapped) => { + return new AWSResponse(unwrapped) + }, +} diff --git a/src/framework/awsLambda/index.ts b/src/framework/awsLambda/index.ts new file mode 100644 index 000000000..bffc78e35 --- /dev/null +++ b/src/framework/awsLambda/index.ts @@ -0,0 +1,21 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { AWSWrapper } from './framework' +export type { SessionEvent, SessionEventV2 } from './framework' + +export const middleware = AWSWrapper.middleware +export const wrapRequest = AWSWrapper.wrapRequest +export const wrapResponse = AWSWrapper.wrapResponse diff --git a/src/framework/constants.ts b/src/framework/constants.ts new file mode 100644 index 000000000..db58948e2 --- /dev/null +++ b/src/framework/constants.ts @@ -0,0 +1,15 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export const COOKIE_HEADER = 'Set-Cookie' diff --git a/src/framework/express/framework.ts b/src/framework/express/framework.ts new file mode 100644 index 000000000..5eb8958f5 --- /dev/null +++ b/src/framework/express/framework.ts @@ -0,0 +1,208 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { NextFunction, Request, Response } from 'express' +import type { HTTPMethod } from '../../types' +import { normaliseHttpMethod } from '../../utils' +import { BaseRequest } from '../request' +import { BaseResponse } from '../response' +import { + assertFormDataBodyParserHasBeenUsedForExpressLikeRequest, + assertThatBodyParserHasBeenUsedForExpressLikeRequest, + getCookieValueFromIncomingMessage, + getHeaderValueFromIncomingMessage, + setCookieForServerResponse, + setHeaderForExpressLikeResponse, +} from '../utils' +import type { Framework } from '../types' +import SuperTokens from '../../supertokens' +import type { SessionContainerInterface } from '../../recipe/session/types' + +export class ExpressRequest extends BaseRequest { + private request: Request + private parserChecked: boolean + private formDataParserChecked: boolean + + constructor(request: Request) { + super() + this.original = request + this.request = request + this.parserChecked = false + this.formDataParserChecked = false + } + + getFormData = async (): Promise => { + if (!this.formDataParserChecked) { + await assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request) + this.formDataParserChecked = true + } + return this.request.body + } + + getKeyValueFromQuery = (key: string): string | undefined => { + if (this.request.query === undefined) + return undefined + + const value = this.request.query[key] + if (value === undefined || typeof value !== 'string') + return undefined + + return value + } + + getJSONBody = async (): Promise => { + if (!this.parserChecked) { + await assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request) + this.parserChecked = true + } + return this.request.body + } + + getMethod = (): HTTPMethod => { + return normaliseHttpMethod(this.request.method) + } + + getCookieValue = (key: string): string | undefined => { + return getCookieValueFromIncomingMessage(this.request, key) + } + + getHeaderValue = (key: string): string | undefined => { + return getHeaderValueFromIncomingMessage(this.request, key) + } + + getOriginalURL = (): string => { + return this.request.originalUrl || this.request.url + } +} + +export class ExpressResponse extends BaseResponse { + private response: Response + private statusCode: number + + constructor(response: Response) { + super() + this.original = response + this.response = response + this.statusCode = 200 + } + + sendHTMLResponse = (html: string) => { + if (!this.response.writableEnded) { + /** + * response.set method is not available if response + * is a nextjs response object. setHeader method + * is present on OutgoingMessage which is one of the + * bases used to construct response object for express + * like response as well as nextjs like response + */ + this.response.setHeader('Content-Type', 'text/html') + this.response.status(this.statusCode).send(Buffer.from(html)) + } + } + + setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { + setHeaderForExpressLikeResponse(this.response, key, value, allowDuplicateKey) + } + + removeHeader = (key: string) => { + this.response.removeHeader(key) + } + + setCookie = ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', + ) => { + setCookieForServerResponse(this.response, key, value, domain, secure, httpOnly, expires, path, sameSite) + } + + /** + * @param {number} statusCode + */ + setStatusCode = (statusCode: number) => { + if (!this.response.writableEnded) + this.statusCode = statusCode + } + + sendJSONResponse = (content: any) => { + if (!this.response.writableEnded) + this.response.status(this.statusCode).json(content) + } +} + +export interface SessionRequest extends Request { + session?: SessionContainerInterface +} + +export const middleware = () => { + return async (req: Request, res: Response, next: NextFunction) => { + let supertokens + const request = new ExpressRequest(req) + const response = new ExpressResponse(res) + try { + supertokens = SuperTokens.getInstanceOrThrowError() + const result = await supertokens.middleware(request, response) + if (!result) + return next() + } + catch (err) { + if (supertokens) { + try { + await supertokens.errorHandler(err, request, response) + } + catch { + next(err) + } + } + else { + next(err) + } + } + } +} +export const errorHandler = () => { + return async (err: any, req: Request, res: Response, next: NextFunction) => { + const supertokens = SuperTokens.getInstanceOrThrowError() + const request = new ExpressRequest(req) + const response = new ExpressResponse(res) + try { + await supertokens.errorHandler(err, request, response) + } + catch (err) { + return next(err) + } + } +} + +export interface ExpressFramework extends Framework { + middleware: () => (req: Request, res: Response, next: NextFunction) => Promise + errorHandler: () => (err: any, req: Request, res: Response, next: NextFunction) => Promise +} + +export const ExpressWrapper: ExpressFramework = { + middleware, + errorHandler, + wrapRequest: (unwrapped) => { + return new ExpressRequest(unwrapped) + }, + wrapResponse: (unwrapped) => { + return new ExpressResponse(unwrapped) + }, +} diff --git a/src/framework/express/index.ts b/src/framework/express/index.ts new file mode 100644 index 000000000..30f2eb31b --- /dev/null +++ b/src/framework/express/index.ts @@ -0,0 +1,22 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { ExpressWrapper } from './framework' +export type { SessionRequest } from './framework' + +export const middleware = ExpressWrapper.middleware +export const errorHandler = ExpressWrapper.errorHandler +export const wrapRequest = ExpressWrapper.wrapRequest +export const wrapResponse = ExpressWrapper.wrapResponse diff --git a/src/framework/fastify/framework.ts b/src/framework/fastify/framework.ts new file mode 100644 index 000000000..e3b37cf28 --- /dev/null +++ b/src/framework/fastify/framework.ts @@ -0,0 +1,233 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { + FastifyInstance, + FastifyPluginCallback, + FastifyReply, + FastifyRequest as OriginalFastifyRequest, +} from 'fastify' +import type { HTTPMethod } from '../../types' +import { getFromObjectCaseInsensitive, normaliseHttpMethod } from '../../utils' +import { BaseRequest } from '../request' +import { BaseResponse } from '../response' +import { getCookieValueFromHeaders, normalizeHeaderValue, serializeCookieValue } from '../utils' +import type { Framework } from '../types' +import SuperTokens from '../../supertokens' +import type { SessionContainerInterface } from '../../recipe/session/types' +import { COOKIE_HEADER } from '../constants' + +export class FastifyRequest extends BaseRequest { + private request: OriginalFastifyRequest + + constructor(request: OriginalFastifyRequest) { + super() + this.original = request + this.request = request + } + + getFormData = async (): Promise => { + return this.request.body // NOTE: ask user to add require('fastify-formbody') + } + + getKeyValueFromQuery = (key: string): string | undefined => { + if (this.request.query === undefined) + return undefined + + const value = (this.request.query as any)[key] + if (value === undefined || typeof value !== 'string') + return undefined + + return value + } + + getJSONBody = async (): Promise => { + return this.request.body + } + + getMethod = (): HTTPMethod => { + return normaliseHttpMethod(this.request.method) + } + + getCookieValue = (key: string): string | undefined => { + return getCookieValueFromHeaders(this.request.headers, key) + } + + getHeaderValue = (key: string): string | undefined => { + return normalizeHeaderValue(getFromObjectCaseInsensitive(key, this.request.headers)) + } + + getOriginalURL = (): string => { + return this.request.url + } +} + +export class FastifyResponse extends BaseResponse { + private response: FastifyReply + private statusCode: number + + constructor(response: FastifyReply) { + super() + this.original = response + this.response = response + this.statusCode = 200 + } + + sendHTMLResponse = (html: string) => { + if (!this.response.sent) { + this.response.type('text/html') + this.response.send(html) + } + } + + setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { + try { + const existingHeaders = this.response.getHeaders() + const existingValue = existingHeaders[key.toLowerCase()] + + // we have the this.response.header for compatibility with nextJS + if (existingValue === undefined) { + this.response.header(key, value) + } + else if (allowDuplicateKey) { + this.response.header(key, `${existingValue}, ${value}`) + } + else { + // we overwrite the current one with the new one + this.response.header(key, value) + } + } + catch (err) { + throw new Error(`Error while setting header with key: ${key} and value: ${value}`) + } + } + + removeHeader = (key: string) => { + this.response.removeHeader(key) + } + + setCookie = ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', + ) => { + const serialisedCookie = serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite) + /** + * lets say if current value is undefined, prev -> undefined + * + * now if add AT, + * cookieValueToSetInHeader -> AT + * response header object will be: + * + * 'set-cookie': AT + * + * now if add RT, + * + * prev -> AT + * cookieValueToSetInHeader -> AT + RT + * and response header object will be: + * + * 'set-cookie': AT + AT + RT + * + * now if add IRT, + * + * prev -> AT + AT + RT + * cookieValueToSetInHeader -> IRT + AT + AT + RT + * and response header object will be: + * + * 'set-cookie': AT + AT + RT + IRT + AT + AT + RT + * + * To avoid this, we no longer get and use the previous value + * + * Old code: + * + * let prev: string | string[] | undefined = this.response.getHeader(COOKIE_HEADER) as + * | string + * | string[] + * | undefined; + * let cookieValueToSetInHeader = getCookieValueToSetInHeader(prev, serialisedCookie, key); + * this.response.header(COOKIE_HEADER, cookieValueToSetInHeader); + */ + this.response.header(COOKIE_HEADER, serialisedCookie) + } + + /** + * @param {number} statusCode + */ + setStatusCode = (statusCode: number) => { + if (!this.response.sent) + this.statusCode = statusCode + } + + /** + * @param {any} content + */ + sendJSONResponse = (content: any) => { + if (!this.response.sent) { + this.response.statusCode = this.statusCode + this.response.send(content) + } + } +} + +function plugin(fastify: FastifyInstance, _: any, done: Function) { + fastify.addHook('preHandler', async (req: OriginalFastifyRequest, reply: FastifyReply) => { + const supertokens = SuperTokens.getInstanceOrThrowError() + const request = new FastifyRequest(req) + const response = new FastifyResponse(reply) + try { + await supertokens.middleware(request, response) + } + catch (err) { + await supertokens.errorHandler(err, request, response) + } + }) + done() +} +(plugin as any)[Symbol.for('skip-override')] = true + +export interface SessionRequest extends OriginalFastifyRequest { + session?: SessionContainerInterface +} + +export interface FasitfyFramework extends Framework { + plugin: FastifyPluginCallback + errorHandler: () => (err: any, req: OriginalFastifyRequest, res: FastifyReply) => Promise +} + +export const errorHandler = () => { + return async (err: any, req: OriginalFastifyRequest, res: FastifyReply) => { + const supertokens = SuperTokens.getInstanceOrThrowError() + const request = new FastifyRequest(req) + const response = new FastifyResponse(res) + await supertokens.errorHandler(err, request, response) + } +} + +export const FastifyWrapper: FasitfyFramework = { + plugin, + errorHandler, + wrapRequest: (unwrapped) => { + return new FastifyRequest(unwrapped) + }, + wrapResponse: (unwrapped) => { + return new FastifyResponse(unwrapped) + }, +} diff --git a/src/framework/fastify/index.ts b/src/framework/fastify/index.ts new file mode 100644 index 000000000..4bd15ee51 --- /dev/null +++ b/src/framework/fastify/index.ts @@ -0,0 +1,22 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { FastifyWrapper } from './framework' +export type { SessionRequest } from './framework' + +export const plugin = FastifyWrapper.plugin +export const errorHandler = FastifyWrapper.errorHandler +export const wrapRequest = FastifyWrapper.wrapRequest +export const wrapResponse = FastifyWrapper.wrapResponse diff --git a/src/framework/hapi/framework.ts b/src/framework/hapi/framework.ts new file mode 100644 index 000000000..5d203bb62 --- /dev/null +++ b/src/framework/hapi/framework.ts @@ -0,0 +1,284 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { Plugin, Request, ResponseObject, ResponseToolkit, ServerRoute } from '@hapi/hapi' +import type { Boom } from '@hapi/boom' +import type { HTTPMethod } from '../../types' +import { normaliseHttpMethod } from '../../utils' +import { BaseRequest } from '../request' +import { BaseResponse } from '../response' +import { getCookieValueFromHeaders, normalizeHeaderValue } from '../utils' +import type { Framework } from '../types' +import SuperTokens from '../../supertokens' +import type { SessionContainerInterface } from '../../recipe/session/types' + +export class HapiRequest extends BaseRequest { + private request: Request + + constructor(request: Request) { + super() + this.original = request + this.request = request + } + + getFormData = async (): Promise => { + return (this.request.payload === undefined || this.request.payload === null) ? {} : this.request.payload + } + + getKeyValueFromQuery = (key: string): string | undefined => { + if (this.request.query === undefined) + return undefined + + const value = this.request.query[key] + if (value === undefined || typeof value !== 'string') + return undefined + + return value + } + + getJSONBody = async (): Promise => { + return (this.request.payload === undefined || this.request.payload === null) ? {} : this.request.payload + } + + getMethod = (): HTTPMethod => { + return normaliseHttpMethod(this.request.method) + } + + getCookieValue = (key: string): string | undefined => { + return getCookieValueFromHeaders(this.request.headers, key) + } + + getHeaderValue = (key: string): string | undefined => { + return normalizeHeaderValue(this.request.headers[key]) + } + + getOriginalURL = (): string => { + return this.request.url.toString() + } +} + +export interface ExtendedResponseToolkit extends ResponseToolkit { + lazyHeaderBindings: ( + h: ResponseToolkit, + key: string, + value: string | undefined, + allowDuplicateKey: boolean + ) => void +} + +export class HapiResponse extends BaseResponse { + private response: ExtendedResponseToolkit + private statusCode: number + private content: any + public responseSet: boolean + public statusSet = false + + constructor(response: ExtendedResponseToolkit) { + super() + this.original = response + this.response = response + this.statusCode = 200 + this.content = null + this.responseSet = false + } + + sendHTMLResponse = (html: string) => { + if (!this.responseSet) { + this.content = html + this.setHeader('Content-Type', 'text/html', false) + this.responseSet = true + } + } + + setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { + try { + this.response.lazyHeaderBindings(this.response, key, value, allowDuplicateKey) + } + catch (err) { + throw new Error(`Error while setting header with key: ${key} and value: ${value}`) + } + } + + removeHeader = (key: string) => { + this.response.lazyHeaderBindings(this.response, key, undefined, false) + } + + setCookie = ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', + ) => { + const now = Date.now() + + if (expires > now) { + this.response.state(key, value, { + isHttpOnly: httpOnly, + isSecure: secure, + path, + domain, + ttl: expires - now, + isSameSite: sameSite === 'lax' ? 'Lax' : sameSite === 'none' ? 'None' : 'Strict', + }) + } + else { + this.response.unstate(key) + } + } + + /** + * @param {number} statusCode + */ + setStatusCode = (statusCode: number) => { + if (!this.statusSet) { + this.statusCode = statusCode + this.statusSet = true + } + } + + /** + * @param {any} content + */ + sendJSONResponse = (content: any) => { + if (!this.responseSet) { + this.content = content + this.responseSet = true + } + } + + sendResponse = (overwriteHeaders = false): ResponseObject => { + if (!overwriteHeaders) + return this.response.response(this.content).code(this.statusCode).takeover() + + return this.response.response(this.content).code(this.statusCode) + } +} + +const plugin: Plugin<{}> = { + name: 'supertokens-hapi-middleware', + version: '1.0.0', + async register(server, _) { + const supertokens = SuperTokens.getInstanceOrThrowError() + server.ext('onPreHandler', async (req, h) => { + const request = new HapiRequest(req) + const response = new HapiResponse(h as ExtendedResponseToolkit) + const result = await supertokens.middleware(request, response) + if (!result) + return h.continue + + return response.sendResponse() + }) + server.ext('onPreResponse', async (request, h) => { + (((request.app as any).lazyHeaders || []) as { + key: string + value: string + allowDuplicateKey: boolean + }[]).forEach(({ key, value, allowDuplicateKey }) => { + if ((request.response as Boom).isBoom) + (request.response as Boom).output.headers[key] = value + else + (request.response as ResponseObject).header(key, value, { append: allowDuplicateKey }) + }) + if ((request.response as Boom).isBoom) { + const err = (request.response as Boom).data || request.response + const req = new HapiRequest(request) + const res = new HapiResponse(h as ExtendedResponseToolkit) + if (err !== undefined && err !== null) { + try { + await supertokens.errorHandler(err, req, res) + if (res.responseSet) { + const resObj = res.sendResponse(true); + (((request.app as any).lazyHeaders || []) as { + key: string + value: string + allowDuplicateKey: boolean + }[]).forEach(({ key, value, allowDuplicateKey }) => { + resObj.header(key, value, { append: allowDuplicateKey }) + }) + return resObj.takeover() + } + return h.continue + } + catch (e) { + return h.continue + } + } + } + return h.continue + }) + server.decorate('toolkit', 'lazyHeaderBindings', ( + h: ResponseToolkit, + key: string, + value: string | undefined, + allowDuplicateKey: boolean, + ) => { + const anyApp = h.request.app as any + anyApp.lazyHeaders = anyApp.lazyHeaders || [] + if (value === undefined) { + anyApp.lazyHeaders = anyApp.lazyHeaders.filter( + (header: { key: string }) => header.key.toLowerCase() !== key.toLowerCase(), + ) + } + else { + anyApp.lazyHeaders.push({ key, value, allowDuplicateKey }) + } + }) + const supportedRoutes: ServerRoute[] = [] + const routeMethodSet = new Set() + for (let i = 0; i < supertokens.recipeModules.length; i++) { + const apisHandled = supertokens.recipeModules[i].getAPIsHandled() + for (let j = 0; j < apisHandled.length; j++) { + const api = apisHandled[j] + if (!api.disabled) { + const path = `${supertokens.appInfo.apiBasePath.getAsStringDangerous()}${api.pathWithoutApiBasePath.getAsStringDangerous()}` + const methodAndPath = `${api.method}-${path}` + if (!routeMethodSet.has(methodAndPath)) { + supportedRoutes.push({ + path, + method: api.method, + handler: (_, h) => { + return h.continue + }, + }) + routeMethodSet.add(methodAndPath) + } + } + } + } + server.route(supportedRoutes) + }, +} + +export interface SessionRequest extends Request { + session?: SessionContainerInterface +} + +export interface HapiFramework extends Framework { + plugin: Plugin<{}> +} + +export const HapiWrapper: HapiFramework = { + plugin, + wrapRequest: (unwrapped) => { + return new HapiRequest(unwrapped) + }, + wrapResponse: (unwrapped) => { + return new HapiResponse(unwrapped) + }, +} diff --git a/src/framework/hapi/index.ts b/src/framework/hapi/index.ts new file mode 100644 index 000000000..f22c9b240 --- /dev/null +++ b/src/framework/hapi/index.ts @@ -0,0 +1,21 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { HapiWrapper } from './framework' +export type { SessionRequest } from './framework' + +export const plugin = HapiWrapper.plugin +export const wrapRequest = HapiWrapper.wrapRequest +export const wrapResponse = HapiWrapper.wrapResponse diff --git a/src/framework/index.ts b/src/framework/index.ts new file mode 100644 index 000000000..6a48b94f1 --- /dev/null +++ b/src/framework/index.ts @@ -0,0 +1,39 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import * as expressFramework from './express' +import * as fastifyFramework from './fastify' +import * as hapiFramework from './hapi' +import * as loopbackFramework from './loopback' +import * as koaFramework from './koa' +import * as awsLambdaFramework from './awsLambda' + +export { BaseRequest } from './request' +export { BaseResponse } from './response' + +export default { + express: expressFramework, + fastify: fastifyFramework, + hapi: hapiFramework, + loopback: loopbackFramework, + koa: koaFramework, + awsLambda: awsLambdaFramework, +} + +export const express = expressFramework +export const fastify = fastifyFramework +export const hapi = hapiFramework +export const loopback = loopbackFramework +export const koa = koaFramework +export const awsLambda = awsLambdaFramework diff --git a/src/framework/koa/framework.ts b/src/framework/koa/framework.ts new file mode 100644 index 000000000..8e670a241 --- /dev/null +++ b/src/framework/koa/framework.ts @@ -0,0 +1,210 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { Context, Next } from 'koa' +import { form, json } from 'co-body' +import type { HTTPMethod } from '../../types' +import { normaliseHttpMethod } from '../../utils' +import { BaseRequest } from '../request' +import { BaseResponse } from '../response' +import { getHeaderValueFromIncomingMessage } from '../utils' +import { SessionContainerInterface } from '../../recipe/session/types' +import SuperTokens from '../../supertokens' +import { Framework } from '../types' + +export class KoaRequest extends BaseRequest { + private ctx: Context + private parsedJSONBody: Object | undefined + private parsedUrlEncodedFormData: Object | undefined + + constructor(ctx: Context) { + super() + this.original = ctx + this.ctx = ctx + this.parsedJSONBody = undefined + this.parsedUrlEncodedFormData = undefined + } + + getFormData = async (): Promise => { + if (this.parsedUrlEncodedFormData === undefined) + this.parsedUrlEncodedFormData = await parseURLEncodedFormData(this.ctx) + + return this.parsedUrlEncodedFormData + } + + getKeyValueFromQuery = (key: string): string | undefined => { + if (this.ctx.query === undefined) + return undefined + + const value = this.ctx.request.query[key] + if (value === undefined || typeof value !== 'string') + return undefined + + return value + } + + getJSONBody = async (): Promise => { + if (this.parsedJSONBody === undefined) + this.parsedJSONBody = await parseJSONBodyFromRequest(this.ctx) + + return this.parsedJSONBody === undefined ? {} : this.parsedJSONBody + } + + getMethod = (): HTTPMethod => { + return normaliseHttpMethod(this.ctx.request.method) + } + + getCookieValue = (key: string): string | undefined => { + return this.ctx.cookies.get(key) + } + + getHeaderValue = (key: string): string | undefined => { + return getHeaderValueFromIncomingMessage(this.ctx.req, key) + } + + getOriginalURL = (): string => { + return this.ctx.originalUrl + } +} + +async function parseJSONBodyFromRequest(ctx: Context) { + if (ctx.body !== undefined) + return ctx.body + + return await json(ctx) +} + +async function parseURLEncodedFormData(ctx: Context) { + if (ctx.body !== undefined) + return ctx.body + + return await form(ctx) +} + +export class KoaResponse extends BaseResponse { + private ctx: Context + public responseSet = false + public statusSet = false + + constructor(ctx: Context) { + super() + this.original = ctx + this.ctx = ctx + } + + sendHTMLResponse = (html: string) => { + if (!this.responseSet) { + this.ctx.set('content-type', 'text/html') + this.ctx.body = html + this.responseSet = true + } + } + + setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { + try { + const existingHeaders = this.ctx.response.headers + const existingValue = existingHeaders[key.toLowerCase()] + + if (existingValue === undefined) { + this.ctx.set(key, value) + } + else if (allowDuplicateKey) { + this.ctx.set(key, `${existingValue}, ${value}`) + } + else { + // we overwrite the current one with the new one + this.ctx.set(key, value) + } + } + catch (err) { + throw new Error(`Error while setting header with key: ${key} and value: ${value}`) + } + } + + removeHeader = (key: string) => { + this.ctx.remove(key) + } + + setCookie = ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', + ) => { + this.ctx.cookies.set(key, value, { + secure, + sameSite, + httpOnly, + expires: new Date(expires), + domain, + path, + }) + } + + /** + * @param {number} statusCode + */ + setStatusCode = (statusCode: number) => { + if (!this.statusSet) { + this.ctx.status = statusCode + this.statusSet = true + } + } + + sendJSONResponse = (content: any) => { + if (!this.responseSet) { + this.ctx.body = content + this.responseSet = true + } + } +} + +export interface SessionContext extends Context { + session?: SessionContainerInterface +} + +export const middleware = () => { + return async (ctx: Context, next: Next) => { + const supertokens = SuperTokens.getInstanceOrThrowError() + const request = new KoaRequest(ctx) + const response = new KoaResponse(ctx) + try { + const result = await supertokens.middleware(request, response) + if (!result) + return await next() + } + catch (err) { + return await supertokens.errorHandler(err, request, response) + } + } +} + +export interface KoaFramework extends Framework { + middleware: () => (ctx: Context, next: Next) => Promise +} + +export const KoaWrapper: KoaFramework = { + middleware, + wrapRequest: (unwrapped) => { + return new KoaRequest(unwrapped) + }, + wrapResponse: (unwrapped) => { + return new KoaResponse(unwrapped) + }, +} diff --git a/src/framework/koa/index.ts b/src/framework/koa/index.ts new file mode 100644 index 000000000..2352e1bcf --- /dev/null +++ b/src/framework/koa/index.ts @@ -0,0 +1,21 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { KoaWrapper } from './framework' +export type { SessionContext } from './framework' + +export const middleware = KoaWrapper.middleware +export const wrapRequest = KoaWrapper.wrapRequest +export const wrapResponse = KoaWrapper.wrapResponse diff --git a/src/framework/loopback/framework.ts b/src/framework/loopback/framework.ts new file mode 100644 index 000000000..ba41b151f --- /dev/null +++ b/src/framework/loopback/framework.ts @@ -0,0 +1,172 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { Middleware, MiddlewareContext, Request, Response } from '@loopback/rest' +import type { Next } from '@loopback/core' +import { SessionContainerInterface } from '../../recipe/session/types' +import { HTTPMethod } from '../../types' +import { normaliseHttpMethod } from '../../utils' +import { BaseRequest } from '../request' +import { BaseResponse } from '../response' +import { + assertFormDataBodyParserHasBeenUsedForExpressLikeRequest, + assertThatBodyParserHasBeenUsedForExpressLikeRequest, + getCookieValueFromIncomingMessage, + getHeaderValueFromIncomingMessage, + setCookieForServerResponse, + setHeaderForExpressLikeResponse, +} from '../utils' +import SuperTokens from '../../supertokens' +import type { Framework } from '../types' + +export class LoopbackRequest extends BaseRequest { + private request: Request + private parserChecked: boolean + private formDataParserChecked: boolean + + constructor(ctx: MiddlewareContext) { + super() + this.original = ctx.request + this.request = ctx.request + this.parserChecked = false + this.formDataParserChecked = false + } + + getFormData = async (): Promise => { + if (!this.formDataParserChecked) { + await assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(this.request) + this.formDataParserChecked = true + } + return this.request.body + } + + getKeyValueFromQuery = (key: string): string | undefined => { + if (this.request.query === undefined) + return undefined + + const value = this.request.query[key] + if (value === undefined || typeof value !== 'string') + return undefined + + return value + } + + getJSONBody = async (): Promise => { + if (!this.parserChecked) { + await assertThatBodyParserHasBeenUsedForExpressLikeRequest(this.getMethod(), this.request) + this.parserChecked = true + } + return this.request.body + } + + getMethod = (): HTTPMethod => { + return normaliseHttpMethod(this.request.method) + } + + getCookieValue = (key: string): string | undefined => { + return getCookieValueFromIncomingMessage(this.request, key) + } + + getHeaderValue = (key: string): string | undefined => { + return getHeaderValueFromIncomingMessage(this.request, key) + } + + getOriginalURL = (): string => { + return this.request.originalUrl + } +} + +export class LoopbackResponse extends BaseResponse { + response: Response + private statusCode: number + + constructor(ctx: MiddlewareContext) { + super() + this.original = ctx.response + this.response = ctx.response + this.statusCode = 200 + } + + sendHTMLResponse = (html: string) => { + if (!this.response.writableEnded) { + this.response.set('Content-Type', 'text/html') + this.response.status(this.statusCode).send(Buffer.from(html)) + } + } + + setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { + setHeaderForExpressLikeResponse(this.response, key, value, allowDuplicateKey) + } + + removeHeader = (key: string) => { + this.response.removeHeader(key) + } + + setCookie = ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', + ) => { + setCookieForServerResponse(this.response, key, value, domain, secure, httpOnly, expires, path, sameSite) + } + + setStatusCode = (statusCode: number) => { + if (!this.response.writableEnded) + this.statusCode = statusCode + } + + sendJSONResponse = (content: any) => { + if (!this.response.writableEnded) + this.response.status(this.statusCode).json(content) + } +} + +export interface SessionContext extends MiddlewareContext { + session?: SessionContainerInterface +} + +export interface LoopbackFramework extends Framework { + middleware: Middleware +} +export const middleware: Middleware = async (ctx: MiddlewareContext, next: Next) => { + const supertokens = SuperTokens.getInstanceOrThrowError() + const request = new LoopbackRequest(ctx) + const response = new LoopbackResponse(ctx) + try { + const result = await supertokens.middleware(request, response) + if (!result) + return await next() + + return + } + catch (err) { + return await supertokens.errorHandler(err, request, response) + } +} + +export const LoopbackWrapper: LoopbackFramework = { + middleware, + wrapRequest: (unwrapped) => { + return new LoopbackRequest(unwrapped) + }, + wrapResponse: (unwrapped) => { + return new LoopbackResponse(unwrapped) + }, +} diff --git a/src/framework/loopback/index.ts b/src/framework/loopback/index.ts new file mode 100644 index 000000000..2ce4cfd31 --- /dev/null +++ b/src/framework/loopback/index.ts @@ -0,0 +1,21 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { LoopbackWrapper } from './framework' +export type { SessionContext } from './framework' + +export const middleware = LoopbackWrapper.middleware +export const wrapRequest = LoopbackWrapper.wrapRequest +export const wrapResponse = LoopbackWrapper.wrapResponse diff --git a/src/framework/request.ts b/src/framework/request.ts new file mode 100644 index 000000000..d6f493535 --- /dev/null +++ b/src/framework/request.ts @@ -0,0 +1,32 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { HTTPMethod } from '../types' + +export abstract class BaseRequest { + wrapperUsed: boolean + original: any + constructor() { + this.wrapperUsed = true + } + + abstract getKeyValueFromQuery: (key: string) => string | undefined + abstract getJSONBody: () => Promise + abstract getMethod: () => HTTPMethod + abstract getCookieValue: (key_: string) => string | undefined + abstract getHeaderValue: (key: string) => string | undefined + abstract getOriginalURL: () => string + abstract getFormData: () => Promise +} diff --git a/src/framework/response.ts b/src/framework/response.ts new file mode 100644 index 000000000..6158101a4 --- /dev/null +++ b/src/framework/response.ts @@ -0,0 +1,39 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export abstract class BaseResponse { + wrapperUsed: boolean + original: any + constructor() { + this.wrapperUsed = true + } + + abstract setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void + abstract removeHeader: (key: string) => void + abstract setCookie: ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none' + ) => void + + abstract setStatusCode: (statusCode: number) => void + abstract sendJSONResponse: (content: any) => void + abstract sendHTMLResponse: (html: string) => void +} diff --git a/src/framework/types.ts b/src/framework/types.ts new file mode 100644 index 000000000..93eb73e29 --- /dev/null +++ b/src/framework/types.ts @@ -0,0 +1,30 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { BaseRequest } from './request' +import { BaseResponse } from './response' + +export type TypeFramework = 'express' | 'fastify' | 'hapi' | 'loopback' | 'koa' | 'awsLambda' + +export const SchemaFramework = { + type: 'string', + enum: ['express', 'fastify', 'hapi', 'loopback', 'koa', 'awsLambda'], +} + +export interface Framework { + wrapRequest: (unwrapped: any) => BaseRequest + + wrapResponse: (unwrapped: any) => BaseResponse +} diff --git a/src/framework/utils.ts b/src/framework/utils.ts new file mode 100644 index 000000000..bcbfa8259 --- /dev/null +++ b/src/framework/utils.ts @@ -0,0 +1,324 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { IncomingMessage } from 'http' +import { ServerResponse } from 'http' +import { parse, serialize } from 'cookie' +import type { Request, Response } from 'express' +import { json, urlencoded } from 'body-parser' +import { NextApiRequest } from 'next' +import STError from '../error' +import type { HTTPMethod } from '../types' +import { getFromObjectCaseInsensitive } from '../utils' +import { COOKIE_HEADER } from './constants' + +export function getCookieValueFromHeaders(headers: any, key: string): string | undefined { + if (headers === undefined || headers === null) + return undefined + + let cookies: any = headers.cookie || headers.Cookie + + if (cookies === undefined) + return undefined + + cookies = parse(cookies) + + // parse JSON cookies + cookies = JSONCookies(cookies) + + return (cookies as any)[key] +} + +export function getCookieValueFromIncomingMessage(request: IncomingMessage, key: string): string | undefined { + if ((request as any).cookies) + return (request as any).cookies[key] + + return getCookieValueFromHeaders(request.headers, key) +} + +export function getHeaderValueFromIncomingMessage(request: IncomingMessage, key: string): string | undefined { + return normalizeHeaderValue(getFromObjectCaseInsensitive(key, request.headers)) +} + +export function normalizeHeaderValue(value: string | string[] | undefined): string | undefined { + if (value === undefined) + return undefined + + if (Array.isArray(value)) + return value[0] + + return value +} + +/** + * Parse JSON cookie string. + * + * @param {String} str + * @return {Object} Parsed object or undefined if not json cookie + * @public + */ + +function JSONCookie(str: string) { + if (typeof str !== 'string' || str.substr(0, 2) !== 'j:') + return undefined + + try { + return JSON.parse(str.slice(2)) + } + catch (err) { + return undefined + } +} + +/** + * Parse JSON cookies. + * + * @param {Object} obj + * @return {Object} + * @public + */ + +function JSONCookies(obj: any) { + const cookies = Object.keys(obj) + let key + let val + + for (let i = 0; i < cookies.length; i++) { + key = cookies[i] + val = JSONCookie(obj[key]) + + if (val) + obj[key] = val + } + + return obj +} + +export async function assertThatBodyParserHasBeenUsedForExpressLikeRequest( + method: HTTPMethod, + request: (Request | NextApiRequest) & { __supertokensFromNextJS?: true }, +) { + // according to https://github.com/supertokens/supertokens-node/issues/33 + if (method === 'post' || method === 'put') { + if (typeof request.body === 'string') { + try { + request.body = JSON.parse(request.body) + } + catch (err) { + if (request.body === '') { + request.body = {} + } + else { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'API input error: Please make sure to pass a valid JSON input in the request body', + }) + } + } + } + else if ( + request.body === undefined + || Buffer.isBuffer(request.body) + || Object.keys(request.body).length === 0 + ) { + // parsing it again to make sure that the request is parsed atleast once by a json parser + const jsonParser = json() + const err = await new Promise((resolve) => { + let resolvedCalled = false + if (request.readable) { + jsonParser(request, new ServerResponse(request), (e) => { + if (!resolvedCalled) { + resolvedCalled = true + resolve(e) + } + }) + } + else { + resolve(undefined) + } + }) + if (err !== undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'API input error: Please make sure to pass a valid JSON input in the request body', + }) + } + } + } + else if (method === 'delete' || method === 'get') { + if (request.query === undefined) { + const parser = urlencoded({ extended: true }) + const err = await new Promise(resolve => parser(request, new ServerResponse(request), resolve)) + if (err !== undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'API input error: Please make sure to pass valid url encoded form in the request body', + }) + } + } + } +} + +export async function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest( + request: (Request | NextApiRequest) & { __supertokensFromNextJS?: true }, +) { + const parser = urlencoded({ extended: true }) + const err = await new Promise((resolve) => { + if (request.readable) { + parser(request, new ServerResponse(request), (e) => { + resolve(e) + }) + } + else { + resolve(undefined) + } + }) + if (err !== undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'API input error: Please make sure to pass valid url encoded form in the request body', + }) + } +} + +export function setHeaderForExpressLikeResponse(res: Response, key: string, value: string, allowDuplicateKey: boolean) { + try { + const existingHeaders = res.getHeaders() + const existingValue = existingHeaders[key.toLowerCase()] + + // we have the res.header for compatibility with nextJS + if (existingValue === undefined) { + if (res.header !== undefined) + res.header(key, value) + else + res.setHeader(key, value) + } + else if (allowDuplicateKey) { + if (res.header !== undefined) + res.header(key, `${existingValue}, ${value}`) + else + res.setHeader(key, `${existingValue}, ${value}`) + } + else { + // we overwrite the current one with the new one + if (res.header !== undefined) + res.header(key, value) + else + res.setHeader(key, value) + } + } + catch (err) { + throw new Error(`Error while setting header with key: ${key} and value: ${value}`) + } +} + +/** + * + * @param res + * @param name + * @param value + * @param domain + * @param secure + * @param httpOnly + * @param expires + * @param path + */ +export function setCookieForServerResponse( + res: ServerResponse, + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', +) { + return appendToServerResponse( + res, + COOKIE_HEADER, + serializeCookieValue(key, value, domain, secure, httpOnly, expires, path, sameSite), + key, + ) +} + +/** + * Append additional header `field` with value `val`. + * + * Example: + * + * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); + * + * @param {ServerResponse} res + * @param {string} field + * @param {string| string[]} val + */ +function appendToServerResponse(res: ServerResponse, field: string, val: string | string[], key: string) { + const prev: string | string[] | undefined = res.getHeader(field) as string | string[] | undefined + res.setHeader(field, getCookieValueToSetInHeader(prev, val, key)) + return res +} + +export function getCookieValueToSetInHeader( + prev: string | string[] | undefined, + val: string | string[], + key: string, +): string | string[] { + let value = val + + if (prev !== undefined) { + // removing existing cookie with the same name + if (Array.isArray(prev)) { + const removedDuplicate = [] + for (let i = 0; i < prev.length; i++) { + const curr = prev[i] + if (!curr.startsWith(key)) + removedDuplicate.push(curr) + } + prev = removedDuplicate + } + else { + if (prev.startsWith(key)) + prev = undefined + } + if (prev !== undefined) + value = Array.isArray(prev) ? prev.concat(val) : Array.isArray(val) ? [prev].concat(val) : [prev, val] + } + + value = Array.isArray(value) ? value.map(String) : String(value) + return value +} + +export function serializeCookieValue( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: 'strict' | 'lax' | 'none', +): string { + const opts = { + domain, + secure, + httpOnly, + expires: new Date(expires), + path, + sameSite, + } + + return serialize(key, value, opts) +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..38b185a9f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,121 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import SuperTokens from './supertokens' +import SuperTokensError from './error' + +export { SuperTokensError } + +// For Express +export default class SuperTokensWrapper { + static init = SuperTokens.init + + static Error = SuperTokensError + + static getAllCORSHeaders() { + return SuperTokens.getInstanceOrThrowError().getAllCORSHeaders() + } + + static getUserCount(includeRecipeIds?: string[]) { + return SuperTokens.getInstanceOrThrowError().getUserCount(includeRecipeIds) + } + + static getUsersOldestFirst(input?: { + limit?: number + paginationToken?: string + includeRecipeIds?: string[] + query?: object + }): Promise<{ + users: { recipeId: string; user: any }[] + nextPaginationToken?: string + }> { + return SuperTokens.getInstanceOrThrowError().getUsers({ + timeJoinedOrder: 'ASC', + ...input, + }) + } + + static getUsersNewestFirst(input?: { + limit?: number + paginationToken?: string + includeRecipeIds?: string[] + query?: object + }): Promise<{ + users: { recipeId: string; user: any }[] + nextPaginationToken?: string + }> { + return SuperTokens.getInstanceOrThrowError().getUsers({ + timeJoinedOrder: 'DESC', + ...input, + }) + } + + static deleteUser(userId: string) { + return SuperTokens.getInstanceOrThrowError().deleteUser({ + userId, + }) + } + + static createUserIdMapping(input: { + superTokensUserId: string + externalUserId: string + externalUserIdInfo?: string + force?: boolean + }) { + return SuperTokens.getInstanceOrThrowError().createUserIdMapping(input) + } + + static getUserIdMapping(input: { userId: string; userIdType?: 'SUPERTOKENS' | 'EXTERNAL' | 'ANY' }) { + return SuperTokens.getInstanceOrThrowError().getUserIdMapping(input) + } + + static deleteUserIdMapping(input: { + userId: string + userIdType?: 'SUPERTOKENS' | 'EXTERNAL' | 'ANY' + force?: boolean + }) { + return SuperTokens.getInstanceOrThrowError().deleteUserIdMapping(input) + } + + static updateOrDeleteUserIdMappingInfo(input: { + userId: string + userIdType?: 'SUPERTOKENS' | 'EXTERNAL' | 'ANY' + externalUserIdInfo?: string + }) { + return SuperTokens.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo(input) + } +} + +export const init = SuperTokensWrapper.init + +export const getAllCORSHeaders = SuperTokensWrapper.getAllCORSHeaders + +export const getUserCount = SuperTokensWrapper.getUserCount + +export const getUsersOldestFirst = SuperTokensWrapper.getUsersOldestFirst + +export const getUsersNewestFirst = SuperTokensWrapper.getUsersNewestFirst + +export const deleteUser = SuperTokensWrapper.deleteUser + +export const createUserIdMapping = SuperTokensWrapper.createUserIdMapping + +export const getUserIdMapping = SuperTokensWrapper.getUserIdMapping + +export const deleteUserIdMapping = SuperTokensWrapper.deleteUserIdMapping + +export const updateOrDeleteUserIdMappingInfo = SuperTokensWrapper.updateOrDeleteUserIdMappingInfo + +export const Error = SuperTokensWrapper.Error diff --git a/src/ingredients/emaildelivery/index.ts b/src/ingredients/emaildelivery/index.ts new file mode 100644 index 000000000..876f75cf5 --- /dev/null +++ b/src/ingredients/emaildelivery/index.ts @@ -0,0 +1,28 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import { EmailDeliveryInterface, TypeInputWithService } from './types' + +export default class EmailDelivery> { + ingredientInterfaceImpl: EmailDeliveryInterface + + constructor(config: TypeInputWithService) { + let builder = new OverrideableBuilder(config.service) + if (config.override !== undefined) + builder = builder.override(config.override) + + this.ingredientInterfaceImpl = builder.build() + } +} diff --git a/src/ingredients/emaildelivery/services/smtp.ts b/src/ingredients/emaildelivery/services/smtp.ts new file mode 100644 index 000000000..3f364d1e0 --- /dev/null +++ b/src/ingredients/emaildelivery/services/smtp.ts @@ -0,0 +1,46 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' + +export interface SMTPServiceConfig { + host: string + from: { + name: string + email: string + } + port: number + secure?: boolean + authUsername?: string + password: string +} + +export interface GetContentResult { + body: string + isHtml: boolean + subject: string + toEmail: string +} + +export type TypeInputSendRawEmail = GetContentResult & { userContext: any } + +export interface ServiceInterface { + sendRawEmail: (input: TypeInputSendRawEmail) => Promise + getContent: (input: T & { userContext: any }) => Promise +} + +export interface TypeInput { + smtpSettings: SMTPServiceConfig + override?: (oI: ServiceInterface, builder: OverrideableBuilder>) => ServiceInterface +} diff --git a/src/ingredients/emaildelivery/types.ts b/src/ingredients/emaildelivery/types.ts new file mode 100644 index 000000000..3d9f6aded --- /dev/null +++ b/src/ingredients/emaildelivery/types.ts @@ -0,0 +1,38 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' + +export interface EmailDeliveryInterface> { + sendEmail: (input: T & { userContext: any }) => Promise +} + +/** + * config class parameter when parent Recipe create a new EmailDeliveryIngredient object via constructor + */ +export interface TypeInput> { + service?: EmailDeliveryInterface + override?: ( + originalImplementation: EmailDeliveryInterface, + builder: OverrideableBuilder> + ) => EmailDeliveryInterface +} + +export interface TypeInputWithService> { + service: EmailDeliveryInterface + override?: ( + originalImplementation: EmailDeliveryInterface, + builder: OverrideableBuilder> + ) => EmailDeliveryInterface +} diff --git a/src/ingredients/smsdelivery/index.ts b/src/ingredients/smsdelivery/index.ts new file mode 100644 index 000000000..71c685094 --- /dev/null +++ b/src/ingredients/smsdelivery/index.ts @@ -0,0 +1,28 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import { SmsDeliveryInterface, TypeInputWithService } from './types' + +export default class SmsDelivery { + ingredientInterfaceImpl: SmsDeliveryInterface + + constructor(config: TypeInputWithService) { + let builder = new OverrideableBuilder(config.service) + if (config.override !== undefined) + builder = builder.override(config.override) + + this.ingredientInterfaceImpl = builder.build() + } +} diff --git a/src/ingredients/smsdelivery/services/supertokens.ts b/src/ingredients/smsdelivery/services/supertokens.ts new file mode 100644 index 000000000..89f178e7f --- /dev/null +++ b/src/ingredients/smsdelivery/services/supertokens.ts @@ -0,0 +1,16 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const SUPERTOKENS_SMS_SERVICE_URL = 'https://api.supertokens.com/0/services/sms' diff --git a/src/ingredients/smsdelivery/services/twilio.ts b/src/ingredients/smsdelivery/services/twilio.ts new file mode 100644 index 000000000..98cd749f4 --- /dev/null +++ b/src/ingredients/smsdelivery/services/twilio.ts @@ -0,0 +1,75 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import { ClientOpts } from 'twilio/lib/base/BaseTwilio' + +/** + * only one of "from" and "messagingServiceSid" should be passed. + * if both are passed, we should throw error to the user + * saying that only one of them should be set. this is because + * both parameters can't be passed while calling twilio API. + * if none of "from" and "messagingServiceSid" is passed, error + * should be thrown. + */ +export type TwilioServiceConfig = + | { + accountSid: string + authToken: string + from: string + opts?: ClientOpts + } + | { + accountSid: string + authToken: string + messagingServiceSid: string + opts?: ClientOpts + } + +export interface GetContentResult { + body: string + toPhoneNumber: string +} + +export type TypeInputSendRawSms = GetContentResult & { userContext: any } & ( + | { + from: string + } + | { + messagingServiceSid: string + } +) + +export interface ServiceInterface { + sendRawSms: (input: TypeInputSendRawSms) => Promise + getContent: (input: T & { userContext: any }) => Promise +} + +export interface TypeInput { + twilioSettings: TwilioServiceConfig + override?: (oI: ServiceInterface, builder: OverrideableBuilder>) => ServiceInterface +} + +export function normaliseUserInputConfig(input: TypeInput): TypeInput { + const from = 'from' in input.twilioSettings ? input.twilioSettings.from : undefined + const messagingServiceSid + = 'messagingServiceSid' in input.twilioSettings ? input.twilioSettings.messagingServiceSid : undefined + if ( + (from === undefined && messagingServiceSid === undefined) + || (from !== undefined && messagingServiceSid !== undefined) + ) + throw new Error('Please pass exactly one of "from" and "messagingServiceSid" config for twilioSettings.') + + return input +} diff --git a/src/ingredients/smsdelivery/types.ts b/src/ingredients/smsdelivery/types.ts new file mode 100644 index 000000000..21d541732 --- /dev/null +++ b/src/ingredients/smsdelivery/types.ts @@ -0,0 +1,38 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' + +export interface SmsDeliveryInterface { + sendSms: (input: T & { userContext: any }) => Promise +} + +/** + * config class parameter when parent Recipe create a new SmsDeliveryIngredient object via constructor + */ +export interface TypeInput { + service?: SmsDeliveryInterface + override?: ( + originalImplementation: SmsDeliveryInterface, + builder: OverrideableBuilder> + ) => SmsDeliveryInterface +} + +export interface TypeInputWithService { + service: SmsDeliveryInterface + override?: ( + originalImplementation: SmsDeliveryInterface, + builder: OverrideableBuilder> + ) => SmsDeliveryInterface +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 000000000..65fde4e9a --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,54 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import debug from 'debug' +import { version } from './version' + +const SUPERTOKENS_DEBUG_NAMESPACE = 'com.supertokens' + +const getFileLocation = () => { + const errorObject = new Error('get file location') + if (errorObject.stack === undefined) { + // should not come here + return 'N/A' + } + // split the error stack into an array with new line as the separator + const errorStack = errorObject.stack.split('\n') + + // find return the first trace which doesnt have the logger.js file + for (let i = 1; i < errorStack.length; i++) { + if (!errorStack[i].includes('logger.js')) { + // retrieve the string between the parenthesis + return errorStack[i].match(/(?<=\().+?(?=\))/g) + } + } + return 'N/A' +} + +/* + The debug logger below can be used to log debug messages in the following format + com.supertokens {t: "2022-03-18T11:15:24.608Z", message: Your message, file: "/home/supertokens-node/lib/build/supertokens.js:231:18" sdkVer: "9.2.0"} +0m +*/ + +function logDebugMessage(message: string) { + if (debug.enabled(SUPERTOKENS_DEBUG_NAMESPACE)) { + debug(SUPERTOKENS_DEBUG_NAMESPACE)( + `{t: "${new Date().toISOString()}", message: \"${message}\", file: \"${getFileLocation()}\" sdkVer: "${version}"}`, + ) + console.log() + } +} + +export { logDebugMessage } diff --git a/src/nextjs.ts b/src/nextjs.ts new file mode 100644 index 000000000..c34448925 --- /dev/null +++ b/src/nextjs.ts @@ -0,0 +1,62 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { errorHandler } from './framework/express' +function next( + request: any, + response: any, + resolve: (value?: any) => void, + reject: (reason?: any) => void, +): (middlewareError?: any) => Promise { + return async function (middlewareError?: any) { + if (middlewareError === undefined) + return resolve() + + await errorHandler()(middlewareError, request, response, (errorHandlerError: any) => { + if (errorHandlerError !== undefined) + return reject(errorHandlerError) + + // do nothing, error handler does not resolve the promise. + }) + } +} +export default class NextJS { + static async superTokensNextWrapper( + middleware: (next: (middlewareError?: any) => void) => Promise, + request: any, + response: any, + ): Promise { + return new Promise((resolve: any, reject: any) => { + request.__supertokensFromNextJS = true + try { + let callbackCalled = false + const result = middleware((err) => { + callbackCalled = true + next(request, response, resolve, reject)(err) + }) + if (!callbackCalled && !response.finished && !response.headersSent) + return resolve(result) + } + catch (err) { + errorHandler()(err, request, response, (errorHandlerError: any) => { + if (errorHandlerError !== undefined) + return reject(errorHandlerError) + + // do nothing, error handler does not resolve the promise. + }) + } + }) + } +} +export const superTokensNextWrapper = NextJS.superTokensNextWrapper diff --git a/src/normalisedURLDomain.ts b/src/normalisedURLDomain.ts new file mode 100644 index 000000000..191a64fe1 --- /dev/null +++ b/src/normalisedURLDomain.ts @@ -0,0 +1,80 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { URL } from 'url' +import { isAnIpAddress } from './utils' + +export default class NormalisedURLDomain { + private value: string + + constructor(url: string) { + this.value = normaliseURLDomainOrThrowError(url) + } + + getAsStringDangerous = () => { + return this.value + } +} + +function normaliseURLDomainOrThrowError(input: string, ignoreProtocol = false): string { + input = input.trim().toLowerCase() + + try { + if (!input.startsWith('http://') && !input.startsWith('https://') && !input.startsWith('supertokens://')) + throw new Error('converting to proper URL') + + const urlObj = new URL(input) + if (ignoreProtocol) { + if (urlObj.hostname.startsWith('localhost') || isAnIpAddress(urlObj.hostname)) + input = `http://${urlObj.host}` + else + input = `https://${urlObj.host}` + } + else { + input = `${urlObj.protocol}//${urlObj.host}` + } + + return input + } + catch (err) {} + // not a valid URL + + if (input.startsWith('/')) + throw new Error('Please provide a valid domain name') + + if (input.indexOf('.') === 0) + input = input.substr(1) + + // If the input contains a . it means they have given a domain name. + // So we try assuming that they have given a domain name + if ( + (input.includes('.') || input.startsWith('localhost')) + && !input.startsWith('http://') + && !input.startsWith('https://') + ) { + input = `https://${input}` + + // at this point, it should be a valid URL. So we test that before doing a recursive call + try { + // TODO: eslint error fix + // eslint-disable-next-line no-new + new URL(input) + return normaliseURLDomainOrThrowError(input, true) + } + catch (err) {} + } + + throw new Error('Please provide a valid domain name') +} diff --git a/src/normalisedURLPath.ts b/src/normalisedURLPath.ts new file mode 100644 index 000000000..ab196c82d --- /dev/null +++ b/src/normalisedURLPath.ts @@ -0,0 +1,110 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { URL } from 'url' + +export default class NormalisedURLPath { + private value: string + + constructor(url: string) { + this.value = normaliseURLPathOrThrowError(url) + } + + startsWith = (other: NormalisedURLPath) => { + return this.value.startsWith(other.value) + } + + appendPath = (other: NormalisedURLPath) => { + return new NormalisedURLPath(this.value + other.value) + } + + getAsStringDangerous = () => { + return this.value + } + + equals = (other: NormalisedURLPath) => { + return this.value === other.value + } + + isARecipePath = () => { + return this.value === '/recipe' || this.value.startsWith('/recipe/') + } +} + +function normaliseURLPathOrThrowError(input: string): string { + input = input.trim().toLowerCase() + + try { + if (!input.startsWith('http://') && !input.startsWith('https://')) + throw new Error('converting to proper URL') + + const urlObj = new URL(input) + input = urlObj.pathname + + if (input.charAt(input.length - 1) === '/') + return input.substr(0, input.length - 1) + + return input + } + catch (err) {} + // not a valid URL + + // If the input contains a . it means they have given a domain name. + // So we try assuming that they have given a domain name + path + if ( + (domainGiven(input) || input.startsWith('localhost')) + && !input.startsWith('http://') + && !input.startsWith('https://') + ) { + input = `http://${input}` + return normaliseURLPathOrThrowError(input) + } + + if (input.charAt(0) !== '/') + input = `/${input}` + + // at this point, we should be able to convert it into a fake URL and recursively call this function. + try { + // test that we can convert this to prevent an infinite loop + // TODO: Do not use 'new' for side effects.eslintno-new + // eslint-disable-next-line no-new + new URL(`http://example.com${input}`) + + return normaliseURLPathOrThrowError(`http://example.com${input}`) + } + catch (err) { + throw new Error('Please provide a valid URL path') + } +} + +function domainGiven(input: string): boolean { + // If no dot, return false. + if (!input.includes('.') || input.startsWith('/')) + return false + + try { + const url = new URL(input) + return url.hostname.includes('.') + } + catch (ignored) {} + + try { + const url = new URL(`http://${input}`) + return url.hostname.includes('.') + } + catch (ignored) {} + + return false +} diff --git a/src/overrideableBuilder/getProxyObject.ts b/src/overrideableBuilder/getProxyObject.ts new file mode 100644 index 000000000..ca4e44727 --- /dev/null +++ b/src/overrideableBuilder/getProxyObject.ts @@ -0,0 +1,19 @@ +import { ProxiedImplementation } from './types' + +export function getProxyObject any)>>(orig: T): T { + const ret: ProxiedImplementation = { + ...orig, + _call: (_, __) => { + throw new Error('This function should only be called through the recipe object') + }, + } + const keys = Object.keys(ret) as (keyof typeof ret)[] + for (const k of keys) { + if (k !== '_call') { + ret[k] = function (this: ProxiedImplementation, ...args: any[]) { + return this._call(k, args) + } as ProxiedImplementation[keyof T] + } + } + return ret +} diff --git a/src/overrideableBuilder/index.ts b/src/overrideableBuilder/index.ts new file mode 100644 index 000000000..6e7de0967 --- /dev/null +++ b/src/overrideableBuilder/index.ts @@ -0,0 +1,66 @@ +import { getProxyObject } from './getProxyObject' +import { NullablePartial } from './types' + +export class OverrideableBuilder> { + private layers: [T, ...NullablePartial[]] + private proxies: T[] + result?: T + + constructor(originalImplementation: T) { + this.layers = [originalImplementation] + this.proxies = [] + } + + override(overrideFunc: (originalImplementation: T, builder: OverrideableBuilder) => T): OverrideableBuilder { + const proxy = getProxyObject(this.layers[0]) as T + const layer = overrideFunc(proxy, this) as NullablePartial + for (const key of Object.keys(this.layers[0]) as (keyof T)[]) { + if (layer[key] === proxy[key] || key === '_call') + delete layer[key] + + else if (layer[key] === undefined) + layer[key] = null + } + + this.layers.push(layer) + this.proxies.push(proxy) + + return this + } + + build() { + if (this.result) + return this.result + + this.result = {} as T + for (const layer of this.layers) { + for (const key of Object.keys(layer) as (keyof T)[]) { + const override = layer[key] + if (override !== undefined) { + if (override === null) + this.result[key] = undefined as T[keyof T] + + else if (typeof override === 'function') + this.result[key] = override.bind(this.result) as T[keyof T] + + else + this.result[key] = override as T[keyof T] + } + } + } + + for (let proxyInd = 0; proxyInd < this.proxies.length; ++proxyInd) { + const proxy = this.proxies[proxyInd]; + (proxy as any)._call = (fname: K, args: any) => { + for (let i = proxyInd; i >= 0; --i) { + const func = this.layers[i][fname] + if (func !== undefined && func !== null) + return func.bind(this.result)(...args) + } + } + } + return this.result + } +} + +export default OverrideableBuilder diff --git a/src/overrideableBuilder/types.ts b/src/overrideableBuilder/types.ts new file mode 100644 index 000000000..6e9df28cd --- /dev/null +++ b/src/overrideableBuilder/types.ts @@ -0,0 +1,9 @@ +export type ProxiedImplementation = { + [P in keyof T]: T[P]; +} & { + _call: (fname: K, args: any[]) => T[K] +} + +export type NullablePartial = { + [P in keyof T]?: null | T[P]; +} diff --git a/src/postSuperTokensInitCallbacks.ts b/src/postSuperTokensInitCallbacks.ts new file mode 100644 index 000000000..35ae7f7dd --- /dev/null +++ b/src/postSuperTokensInitCallbacks.ts @@ -0,0 +1,29 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export class PostSuperTokensInitCallbacks { + static postInitCallbacks: (() => void)[] = [] + + static addPostInitCallback(cb: () => void) { + PostSuperTokensInitCallbacks.postInitCallbacks.push(cb) + } + + static runPostInitCallbacks() { + for (const cb of PostSuperTokensInitCallbacks.postInitCallbacks) + cb() + + PostSuperTokensInitCallbacks.postInitCallbacks = [] + } +} diff --git a/src/processState.ts b/src/processState.ts new file mode 100644 index 000000000..8f27b7691 --- /dev/null +++ b/src/processState.ts @@ -0,0 +1,76 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export enum PROCESS_STATE { + CALLING_SERVICE_IN_VERIFY, + CALLING_SERVICE_IN_GET_HANDSHAKE_INFO, + CALLING_SERVICE_IN_GET_API_VERSION, + CALLING_SERVICE_IN_REQUEST_HELPER, +} + +export class ProcessState { + history: PROCESS_STATE[] = [] + private static instance: ProcessState | undefined + + private constructor() {} + + static getInstance() { + if (ProcessState.instance === undefined) + ProcessState.instance = new ProcessState() + + return ProcessState.instance + } + + addState = (state: PROCESS_STATE) => { + if (process.env.TEST_MODE === 'testing') + this.history.push(state) + } + + private getEventByLastEventByName = (state: PROCESS_STATE) => { + for (let i = this.history.length - 1; i >= 0; i--) { + if (this.history[i] === state) + return this.history[i] + } + return undefined + } + + reset = () => { + this.history = [] + } + + waitForEvent = async (state: PROCESS_STATE, timeInMS = 7000) => { + const startTime = Date.now() + return new Promise((resolve) => { + // TODO: Unexpected aliasing of 'this' to local variable.eslint@typescript-eslint/no-this-alias + // eslint-disable-next-line @typescript-eslint/no-this-alias + const actualThis = this + function tryAndGet() { + const result = actualThis.getEventByLastEventByName(state) + if (result === undefined) { + if (Date.now() - startTime > timeInMS) + resolve(undefined) + else + setTimeout(tryAndGet, 1000) + } + else { + resolve(result) + } + } + tryAndGet() + }) + } +} + +export default ProcessState diff --git a/src/querier.ts b/src/querier.ts new file mode 100644 index 000000000..b5b0f2068 --- /dev/null +++ b/src/querier.ts @@ -0,0 +1,287 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios from 'axios' + +import { getLargestVersionFromIntersection } from './utils' +import { cdiSupported } from './version' +import NormalisedURLDomain from './normalisedURLDomain' +import NormalisedURLPath from './normalisedURLPath' +import { PROCESS_STATE, ProcessState } from './processState' + +export class Querier { + private static initCalled = false + private static hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined = undefined + private static apiKey: string | undefined = undefined + private static apiVersion: string | undefined = undefined + + private static lastTriedIndex = 0 + private static hostsAliveForTesting: Set = new Set() + + private __hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined + private rIdToCore: string | undefined + + // we have rIdToCore so that recipes can force change the rId sent to core. This is a hack until the core is able + // to support multiple rIds per API + private constructor( + hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined, + rIdToCore?: string, + ) { + this.__hosts = hosts + this.rIdToCore = rIdToCore + } + + getAPIVersion = async (): Promise => { + if (Querier.apiVersion !== undefined) + return Querier.apiVersion + + ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION) + const response = await this.sendRequestHelper( + new NormalisedURLPath('/apiversion'), + 'GET', + (url: string) => { + let headers: any = {} + if (Querier.apiKey !== undefined) { + headers = { + 'api-key': Querier.apiKey, + } + } + return axios.get(url, { + headers, + }) + }, + this.__hosts?.length || 0, + ) + const cdiSupportedByServer: string[] = response.versions + const supportedVersion = getLargestVersionFromIntersection(cdiSupportedByServer, cdiSupported) + if (supportedVersion === undefined) { + throw new Error( + 'The running SuperTokens core version is not compatible with this NodeJS SDK. Please visit https://supertokens.io/docs/community/compatibility to find the right versions', + ) + } + Querier.apiVersion = supportedVersion + return Querier.apiVersion + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Querier.initCalled = false + } + + getHostsAliveForTesting = () => { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + return Querier.hostsAliveForTesting + } + + static getNewInstanceOrThrowError(rIdToCore?: string): Querier { + if (!Querier.initCalled) + throw new Error('Please call the supertokens.init function before using SuperTokens') + + return new Querier(Querier.hosts, rIdToCore) + } + + static init(hosts?: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[], apiKey?: string) { + if (!Querier.initCalled) { + Querier.initCalled = true + Querier.hosts = hosts + Querier.apiKey = apiKey + Querier.apiVersion = undefined + Querier.lastTriedIndex = 0 + Querier.hostsAliveForTesting = new Set() + } + } + + // path should start with "/" + sendPostRequest = async (path: NormalisedURLPath, body: any): Promise => { + return this.sendRequestHelper( + path, + 'POST', + async (url: string) => { + const apiVersion = await this.getAPIVersion() + let headers: any = { + 'cdi-version': apiVersion, + 'content-type': 'application/json; charset=utf-8', + } + if (Querier.apiKey !== undefined) { + headers = { + ...headers, + 'api-key': Querier.apiKey, + } + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = { + ...headers, + rid: this.rIdToCore, + } + } + return await axios({ + method: 'POST', + url, + data: body, + headers, + }) + }, + this.__hosts?.length || 0, + ) + } + + // path should start with "/" + sendDeleteRequest = async (path: NormalisedURLPath, body: any, params?: any): Promise => { + return this.sendRequestHelper( + path, + 'DELETE', + async (url: string) => { + const apiVersion = await this.getAPIVersion() + let headers: any = { 'cdi-version': apiVersion } + if (Querier.apiKey !== undefined) { + headers = { + ...headers, + 'api-key': Querier.apiKey, + 'content-type': 'application/json; charset=utf-8', + } + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = { + ...headers, + rid: this.rIdToCore, + } + } + return await axios({ + method: 'DELETE', + url, + data: body, + headers, + params, + }) + }, + this.__hosts?.length || 0, + ) + } + + // path should start with "/" + sendGetRequest = async (path: NormalisedURLPath, params: any): Promise => { + return this.sendRequestHelper( + path, + 'GET', + async (url: string) => { + const apiVersion = await this.getAPIVersion() + let headers: any = { 'cdi-version': apiVersion } + if (Querier.apiKey !== undefined) { + headers = { + ...headers, + 'api-key': Querier.apiKey, + } + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = { + ...headers, + rid: this.rIdToCore, + } + } + return await axios.get(url, { + params, + headers, + }) + }, + this.__hosts?.length || 0, + ) + } + + // path should start with "/" + sendPutRequest = async (path: NormalisedURLPath, body: any): Promise => { + return this.sendRequestHelper( + path, + 'PUT', + async (url: string) => { + const apiVersion = await this.getAPIVersion() + let headers: any = { 'cdi-version': apiVersion, 'content-type': 'application/json; charset=utf-8' } + if (Querier.apiKey !== undefined) { + headers = { + ...headers, + 'api-key': Querier.apiKey, + } + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = { + ...headers, + rid: this.rIdToCore, + } + } + return await axios({ + method: 'PUT', + url, + data: body, + headers, + }) + }, + this.__hosts?.length || 0, + ) + } + + // path should start with "/" + private sendRequestHelper = async ( + path: NormalisedURLPath, + method: string, + axiosFunction: (url: string) => Promise, + numberOfTries: number, + ): Promise => { + if (this.__hosts === undefined) { + throw new Error( + 'No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using.', + ) + } + if (numberOfTries === 0) + throw new Error('No SuperTokens core available to query') + + const currentDomain: string = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous() + const currentBasePath: string = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous() + Querier.lastTriedIndex++ + Querier.lastTriedIndex = Querier.lastTriedIndex % this.__hosts.length + try { + ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER) + const response = await axiosFunction(currentDomain + currentBasePath + path.getAsStringDangerous()) + if (process.env.TEST_MODE === 'testing') + Querier.hostsAliveForTesting.add(currentDomain + currentBasePath) + + if (response.status !== 200) + throw response + + return response.data + } + catch (err: any) { + if (err.message !== undefined && err.message.includes('ECONNREFUSED')) + return await this.sendRequestHelper(path, method, axiosFunction, numberOfTries - 1) + + if (err.response !== undefined && err.response.status !== undefined && err.response.data !== undefined) { + throw new Error( + `SuperTokens core threw an error for a ${ + method + } request to path: '${ + path.getAsStringDangerous() + }' with status code: ${ + err.response.status + } and message: ${ + err.response.data}`, + ) + } + else { + throw err + } + } + } +} diff --git a/src/recipe/dashboard/api/analytics.ts b/src/recipe/dashboard/api/analytics.ts new file mode 100644 index 000000000..59d24d667 --- /dev/null +++ b/src/recipe/dashboard/api/analytics.ts @@ -0,0 +1,99 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import axios from 'axios' +import { APIInterface, APIOptions } from '../types' +import SuperTokens from '../../../supertokens' +import { Querier } from '../../../querier' +import NormalisedURLPath from '../../../normalisedURLPath' +import { version as SDKVersion } from '../../../version' +import STError from '../../../error' + +export interface Response { + status: 'OK' +} + +export default async function analyticsPost(_: APIInterface, options: APIOptions): Promise { + // If telemetry is disabled, dont send any event + if (!SuperTokens.getInstanceOrThrowError().telemetryEnabled) { + return { + status: 'OK', + } + } + + const { email, dashboardVersion } = await options.req.getJSONBody() + + if (email === undefined) { + throw new STError({ + message: 'Missing required property \'email\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (dashboardVersion === undefined) { + throw new STError({ + message: 'Missing required property \'dashboardVersion\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + let telemetryId: string | undefined + let numberOfUsers: number + try { + const querier = Querier.getNewInstanceOrThrowError(options.recipeId) + const response = await querier.sendGetRequest(new NormalisedURLPath('/telemetry'), {}) + if (response.exists) + telemetryId = response.telemetryId + + numberOfUsers = await SuperTokens.getInstanceOrThrowError().getUserCount() + } + catch (_) { + // If either telemetry id API or user count fetch fails, no event should be sent + return { + status: 'OK', + } + } + + const { apiDomain, websiteDomain, appName } = options.appInfo + const data = { + websiteDomain: websiteDomain.getAsStringDangerous(), + apiDomain: apiDomain.getAsStringDangerous(), + appName, + sdk: 'node', + sdkVersion: SDKVersion, + telemetryId, + numberOfUsers, + email, + dashboardVersion, + } + + try { + await axios({ + url: 'https://api.supertokens.com/0/st/telemetry', + method: 'POST', + data, + headers: { + 'api-version': 3, + }, + }) + } + catch (e) { + // Ignored + } + + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/apiKeyProtector.ts b/src/recipe/dashboard/api/apiKeyProtector.ts new file mode 100644 index 000000000..520dde22b --- /dev/null +++ b/src/recipe/dashboard/api/apiKeyProtector.ts @@ -0,0 +1,38 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { makeDefaultUserContextFromAPI } from '../../../utils' +import { APIFunction, APIInterface, APIOptions } from '../types' +import { sendUnauthorisedAccess } from '../utils' + +export default async function apiKeyProtector( + apiImplementation: APIInterface, + options: APIOptions, + apiFunction: APIFunction, +): Promise { + const shouldAllowAccess = await options.recipeImplementation.shouldAllowAccess({ + req: options.req, + config: options.config, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + if (!shouldAllowAccess) { + sendUnauthorisedAccess(options.res) + return true + } + + const response = await apiFunction(apiImplementation, options) + options.res.sendJSONResponse(response) + return true +} diff --git a/src/recipe/dashboard/api/dashboard.ts b/src/recipe/dashboard/api/dashboard.ts new file mode 100644 index 000000000..f49d0de1a --- /dev/null +++ b/src/recipe/dashboard/api/dashboard.ts @@ -0,0 +1,31 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI } from '../../../utils' +import { APIInterface, APIOptions } from '../types' + +export default async function dashboard(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.dashboardGET === undefined) + return false + + const htmlString = await apiImplementation.dashboardGET({ + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + options.res.sendHTMLResponse(htmlString) + + return true +} diff --git a/src/recipe/dashboard/api/implementation.ts b/src/recipe/dashboard/api/implementation.ts new file mode 100644 index 000000000..26da2bea8 --- /dev/null +++ b/src/recipe/dashboard/api/implementation.ts @@ -0,0 +1,75 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import NormalisedURLDomain from '../../../normalisedURLDomain' +import NormalisedURLPath from '../../../normalisedURLPath' +import SuperTokens from '../../../supertokens' +import { DASHBOARD_API } from '../constants' +import { APIInterface, AuthMode } from '../types' +import { Querier } from '../../../querier' +import { maxVersion } from '../../../utils' + +export default function getAPIImplementation(): APIInterface { + return { + async dashboardGET(input) { + const bundleBasePathString = await input.options.recipeImplementation.getDashboardBundleLocation({ + userContext: input.userContext, + }) + + const bundleDomain + = new NormalisedURLDomain(bundleBasePathString).getAsStringDangerous() + + new NormalisedURLPath(bundleBasePathString).getAsStringDangerous() + + let connectionURI = '' + const superTokensInstance = SuperTokens.getInstanceOrThrowError() + + const authMode: AuthMode = input.options.config.authMode + + if (superTokensInstance.supertokens !== undefined) + connectionURI = superTokensInstance.supertokens.connectionURI + + let isSearchEnabled = false + const cdiVersion = await Querier.getNewInstanceOrThrowError(input.options.recipeId).getAPIVersion() + if (maxVersion('2.20', cdiVersion) === cdiVersion) { + // Only enable search if CDI version is 2.20 or above + isSearchEnabled = true + } + + return ` + + + + + + + + + + +
+ + + ` + }, + } +} diff --git a/src/recipe/dashboard/api/search/tagsGet.ts b/src/recipe/dashboard/api/search/tagsGet.ts new file mode 100644 index 000000000..d82a4acc7 --- /dev/null +++ b/src/recipe/dashboard/api/search/tagsGet.ts @@ -0,0 +1,26 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { APIInterface, APIOptions } from '../../types' +import { Querier } from '../../../../querier' +import NormalisedURLPath from '../../../../normalisedURLPath' + +interface TagsResponse { status: 'OK'; tags: string[] } + +export const getSearchTags = async (_: APIInterface, options: APIOptions): Promise => { + const querier = Querier.getNewInstanceOrThrowError(options.recipeId) + const tagsResponse = await querier.sendGetRequest(new NormalisedURLPath('/user/search/tags'), {}) + return tagsResponse +} diff --git a/src/recipe/dashboard/api/signIn.ts b/src/recipe/dashboard/api/signIn.ts new file mode 100644 index 000000000..fc71491e4 --- /dev/null +++ b/src/recipe/dashboard/api/signIn.ts @@ -0,0 +1,56 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { APIInterface, APIOptions } from '../types' +import { send200Response } from '../../../utils' +import STError from '../../../error' +import { Querier } from '../../../querier' +import NormalisedURLPath from '../../../normalisedURLPath' + +type SignInResponse = + | { status: 'OK'; sessionId: string } + | { status: 'INVALID_CREDENTIALS_ERROR' } + | { status: 'USER_SUSPENDED_ERROR' } + +export default async function signIn(_: APIInterface, options: APIOptions): Promise { + const { email, password } = await options.req.getJSONBody() + + if (email === undefined) { + throw new STError({ + message: 'Missing required parameter \'email\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (password === undefined) { + throw new STError({ + message: 'Missing required parameter \'password\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + const querier = Querier.getNewInstanceOrThrowError(undefined) + const signInResponse = await querier.sendPostRequest( + new NormalisedURLPath('/recipe/dashboard/signin'), + { + email, + password, + }, + ) + + send200Response(options.res, signInResponse) + + return true +} diff --git a/src/recipe/dashboard/api/signOut.ts b/src/recipe/dashboard/api/signOut.ts new file mode 100644 index 000000000..ba8c44fbc --- /dev/null +++ b/src/recipe/dashboard/api/signOut.ts @@ -0,0 +1,36 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { APIInterface, APIOptions } from '../types' +import { send200Response } from '../../../utils' +import { Querier } from '../../../querier' +import NormalisedURLPath from '../../../normalisedURLPath' + +export default async function signOut(_: APIInterface, options: APIOptions): Promise { + if (options.config.authMode === 'api-key') { + send200Response(options.res, { status: 'OK' }) + } + else { + const sessionIdFormAuthHeader = options.req.getHeaderValue('authorization')?.split(' ')[1] + const querier = Querier.getNewInstanceOrThrowError(undefined) + const sessionDeleteResponse = await querier.sendDeleteRequest( + new NormalisedURLPath('/recipe/dashboard/session'), + {}, + { sessionId: sessionIdFormAuthHeader }, + ) + send200Response(options.res, sessionDeleteResponse) + } + return true +} diff --git a/src/recipe/dashboard/api/userdetails/userDelete.ts b/src/recipe/dashboard/api/userdetails/userDelete.ts new file mode 100644 index 000000000..88fbfa192 --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userDelete.ts @@ -0,0 +1,26 @@ +import SuperTokens from '../../../../supertokens' +import { APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' + +interface Response { + status: 'OK' +} + +export const userDelete = async (_: APIInterface, options: APIOptions): Promise => { + const userId = options.req.getKeyValueFromQuery('userId') + + if (userId === undefined) { + throw new STError({ + message: 'Missing required parameter \'userId\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + await SuperTokens.getInstanceOrThrowError().deleteUser({ + userId, + }) + + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts b/src/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts new file mode 100644 index 000000000..ce102894d --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts @@ -0,0 +1,40 @@ +import { APIFunction, APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import EmailVerificationRecipe from '../../../emailverification/recipe' +import EmailVerification from '../../../emailverification' + +type Response = + | { + status: 'OK' + isVerified: boolean + } + | { + status: 'FEATURE_NOT_ENABLED_ERROR' + } + +export const userEmailverifyGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { + const req = options.req + const userId = req.getKeyValueFromQuery('userId') + + if (userId === undefined) { + throw new STError({ + message: 'Missing required parameter \'userId\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + try { + EmailVerificationRecipe.getInstanceOrThrowError() + } + catch (e) { + return { + status: 'FEATURE_NOT_ENABLED_ERROR', + } + } + + const response = await EmailVerification.isEmailVerified(userId) + return { + status: 'OK', + isVerified: response, + } +} diff --git a/src/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts b/src/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts new file mode 100644 index 000000000..daad87dcc --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts @@ -0,0 +1,51 @@ +import { APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import EmailVerification from '../../../emailverification' + +interface Response { + status: 'OK' +} + +export const userEmailVerifyPut = async (_: APIInterface, options: APIOptions): Promise => { + const requestBody = await options.req.getJSONBody() + const userId = requestBody.userId + const verified = requestBody.verified + + if (userId === undefined || typeof userId !== 'string') { + throw new STError({ + message: 'Required parameter \'userId\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (verified === undefined || typeof verified !== 'boolean') { + throw new STError({ + message: 'Required parameter \'verified\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (verified) { + const tokenResponse = await EmailVerification.createEmailVerificationToken(userId) + + if (tokenResponse.status === 'EMAIL_ALREADY_VERIFIED_ERROR') { + return { + status: 'OK', + } + } + + const verifyResponse = await EmailVerification.verifyEmailUsingToken(tokenResponse.token) + + if (verifyResponse.status === 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR') { + // This should never happen because we consume the token immediately after creating it + throw new Error('Should not come here') + } + } + else { + await EmailVerification.unverifyEmail(userId) + } + + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts b/src/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts new file mode 100644 index 000000000..bfda98b2e --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts @@ -0,0 +1,53 @@ +import { APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import EmailVerification from '../../../emailverification' +import EmailVerificationRecipe from '../../../emailverification/recipe' +import { getEmailVerifyLink } from '../../../emailverification/utils' + +interface Response { + status: 'OK' | 'EMAIL_ALREADY_VERIFIED_ERROR' +} + +export const userEmailVerifyTokenPost = async (_: APIInterface, options: APIOptions): Promise => { + const requestBody = await options.req.getJSONBody() + const userId = requestBody.userId + + if (userId === undefined || typeof userId !== 'string') { + throw new STError({ + message: 'Required parameter \'userId\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + const emailResponse = await EmailVerificationRecipe.getInstanceOrThrowError().getEmailForUserId(userId, {}) + + if (emailResponse.status !== 'OK') + throw new Error('Should never come here') + + const emailVerificationToken = await EmailVerification.createEmailVerificationToken(userId) + + if (emailVerificationToken.status === 'EMAIL_ALREADY_VERIFIED_ERROR') { + return { + status: 'EMAIL_ALREADY_VERIFIED_ERROR', + } + } + + const emailVerifyLink = getEmailVerifyLink({ + appInfo: options.appInfo, + token: emailVerificationToken.token, + recipeId: EmailVerificationRecipe.RECIPE_ID, + }) + + await EmailVerification.sendEmail({ + type: 'EMAIL_VERIFICATION', + user: { + id: userId, + email: emailResponse.email, + }, + emailVerifyLink, + }) + + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/userdetails/userGet.ts b/src/recipe/dashboard/api/userdetails/userGet.ts new file mode 100644 index 000000000..2c78fa7ff --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userGet.ts @@ -0,0 +1,109 @@ +import { + APIFunction, + APIInterface, + APIOptions, + EmailPasswordUser, + PasswordlessUser, + ThirdPartyUser, +} from '../../types' +import STError from '../../../../error' +import { getUserForRecipeId, isRecipeInitialised, isValidRecipeId } from '../../utils' +import UserMetaDataRecipe from '../../../usermetadata/recipe' +import UserMetaData from '../../../usermetadata' + +type Response = + | { + status: 'NO_USER_FOUND_ERROR' + } + | { + status: 'RECIPE_NOT_INITIALISED' + } + | { + status: 'OK' + recipeId: 'emailpassword' + user: EmailPasswordUser + } + | { + status: 'OK' + recipeId: 'thirdparty' + user: ThirdPartyUser + } + | { + status: 'OK' + recipeId: 'passwordless' + user: PasswordlessUser + } + +export const userGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { + const userId = options.req.getKeyValueFromQuery('userId') + const recipeId = options.req.getKeyValueFromQuery('recipeId') + + if (userId === undefined) { + throw new STError({ + message: 'Missing required parameter \'userId\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (recipeId === undefined) { + throw new STError({ + message: 'Missing required parameter \'recipeId\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (!isValidRecipeId(recipeId)) { + throw new STError({ + message: 'Invalid recipe id', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (!isRecipeInitialised(recipeId)) { + return { + status: 'RECIPE_NOT_INITIALISED', + } + } + + let user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined = ( + await getUserForRecipeId(userId, recipeId) + ).user + + if (user === undefined) { + return { + status: 'NO_USER_FOUND_ERROR', + } + } + + try { + UserMetaDataRecipe.getInstanceOrThrowError() + } + catch (_) { + user = { + ...user, + firstName: 'FEATURE_NOT_ENABLED', + lastName: 'FEATURE_NOT_ENABLED', + } + + return { + status: 'OK', + recipeId: recipeId as any, + user, + } + } + + const userMetaData = await UserMetaData.getUserMetadata(userId) + const { first_name, last_name } = userMetaData.metadata + + user = { + ...user, + firstName: first_name === undefined ? '' : first_name, + lastName: last_name === undefined ? '' : last_name, + } + + return { + status: 'OK', + recipeId: recipeId as any, + user, + } +} diff --git a/src/recipe/dashboard/api/userdetails/userMetadataGet.ts b/src/recipe/dashboard/api/userdetails/userMetadataGet.ts new file mode 100644 index 000000000..60c4cb0e2 --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userMetadataGet.ts @@ -0,0 +1,39 @@ +import { APIFunction, APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import UserMetaDataRecipe from '../../../usermetadata/recipe' +import UserMetaData from '../../../usermetadata' + +type Response = + | { + status: 'FEATURE_NOT_ENABLED_ERROR' + } + | { + status: 'OK' + data: any + } + +export const userMetaDataGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { + const userId = options.req.getKeyValueFromQuery('userId') + + if (userId === undefined) { + throw new STError({ + message: 'Missing required parameter \'userId\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + try { + UserMetaDataRecipe.getInstanceOrThrowError() + } + catch (e) { + return { + status: 'FEATURE_NOT_ENABLED_ERROR', + } + } + + const metaDataResponse = UserMetaData.getUserMetadata(userId) + return { + status: 'OK', + data: (await metaDataResponse).metadata, + } +} diff --git a/src/recipe/dashboard/api/userdetails/userMetadataPut.ts b/src/recipe/dashboard/api/userdetails/userMetadataPut.ts new file mode 100644 index 000000000..166b34597 --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userMetadataPut.ts @@ -0,0 +1,69 @@ +import { APIInterface, APIOptions } from '../../types' +import UserMetadaRecipe from '../../../usermetadata/recipe' +import UserMetaData from '../../../usermetadata' +import STError from '../../../../error' + +interface Response { + status: 'OK' +} + +export const userMetadataPut = async (_: APIInterface, options: APIOptions): Promise => { + const requestBody = await options.req.getJSONBody() + const userId = requestBody.userId + const data = requestBody.data + + // This is to throw an error early in case the recipe has not been initialised + UserMetadaRecipe.getInstanceOrThrowError() + + if (userId === undefined || typeof userId !== 'string') { + throw new STError({ + message: 'Required parameter \'userId\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (data === undefined || typeof data !== 'string') { + throw new STError({ + message: 'Required parameter \'data\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + // Make sure that data is a valid JSON, this will throw + try { + const parsedData = JSON.parse(data) + + if (typeof parsedData !== 'object') + throw new Error('parsedData is not an object') + + if (Array.isArray(parsedData)) + throw new Error('parsedData is an array') + + if (parsedData === null) + throw new Error('parsedData is null') + } + catch (e) { + throw new STError({ + message: '\'data\' must be a valid JSON body', + type: STError.BAD_INPUT_ERROR, + }) + } + + /** + * This API is meant to set the user metadata of a user. We delete the existing data + * before updating it because we want to make sure that shallow merging does not result + * in the data being incorrect + * + * For example if the old data is {test: "test", test2: "test2"} and the user wants to delete + * test2 from the data simply calling updateUserMetadata with {test: "test"} would not remove + * test2 because of shallow merging. + * + * Removing first ensures that the final data is exactly what the user wanted it to be + */ + await UserMetaData.clearUserMetadata(userId) + await UserMetaData.updateUserMetadata(userId, JSON.parse(data)) + + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/userdetails/userPasswordPut.ts b/src/recipe/dashboard/api/userdetails/userPasswordPut.ts new file mode 100644 index 000000000..77d4a2c7a --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userPasswordPut.ts @@ -0,0 +1,123 @@ +import { APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import EmailPasswordRecipe from '../../../emailpassword/recipe' +import EmailPassword from '../../../emailpassword' +import ThirdPartyEmailPasswordRecipe from '../../../thirdpartyemailpassword/recipe' +import ThirdPartyEmailPassword from '../../../thirdpartyemailpassword' +import { FORM_FIELD_PASSWORD_ID } from '../../../emailpassword/constants' + +type Response = + | { + status: 'OK' + } + | { + status: 'INVALID_PASSWORD_ERROR' + error: string + } + +export const userPasswordPut = async (_: APIInterface, options: APIOptions): Promise => { + const requestBody = await options.req.getJSONBody() + const userId = requestBody.userId + const newPassword = requestBody.newPassword + + if (userId === undefined || typeof userId !== 'string') { + throw new STError({ + message: 'Required parameter \'userId\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (newPassword === undefined || typeof newPassword !== 'string') { + throw new STError({ + message: 'Required parameter \'newPassword\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + let recipeToUse: 'emailpassword' | 'thirdpartyemailpassword' | undefined + + try { + EmailPasswordRecipe.getInstanceOrThrowError() + recipeToUse = 'emailpassword' + } + catch (_) {} + + if (recipeToUse === undefined) { + try { + ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + recipeToUse = 'thirdpartyemailpassword' + } + catch (_) {} + } + + if (recipeToUse === undefined) { + // This means that neither emailpassword or thirdpartyemailpassword is initialised + throw new Error('Should never come here') + } + + if (recipeToUse === 'emailpassword') { + const passwordFormFields = EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( + field => field.id === FORM_FIELD_PASSWORD_ID, + ) + + const passwordValidationError = await passwordFormFields[0].validate(newPassword) + + if (passwordValidationError !== undefined) { + return { + status: 'INVALID_PASSWORD_ERROR', + error: passwordValidationError, + } + } + + const passwordResetToken = await EmailPassword.createResetPasswordToken(userId) + + if (passwordResetToken.status === 'UNKNOWN_USER_ID_ERROR') { + // Techincally it can but its an edge case so we assume that it wont + throw new Error('Should never come here') + } + + const passwordResetResponse = await EmailPassword.resetPasswordUsingToken( + passwordResetToken.token, + newPassword, + ) + + if (passwordResetResponse.status === 'RESET_PASSWORD_INVALID_TOKEN_ERROR') + throw new Error('Should never come here') + + return { + status: 'OK', + } + } + + const passwordFormFields = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( + field => field.id === FORM_FIELD_PASSWORD_ID, + ) + + const passwordValidationError = await passwordFormFields[0].validate(newPassword) + + if (passwordValidationError !== undefined) { + return { + status: 'INVALID_PASSWORD_ERROR', + error: passwordValidationError, + } + } + + const passwordResetToken = await ThirdPartyEmailPassword.createResetPasswordToken(userId) + + if (passwordResetToken.status === 'UNKNOWN_USER_ID_ERROR') { + // Techincally it can but its an edge case so we assume that it wont + throw new Error('Should never come here') + } + + const passwordResetResponse = await ThirdPartyEmailPassword.resetPasswordUsingToken( + passwordResetToken.token, + newPassword, + ) + + if (passwordResetResponse.status === 'RESET_PASSWORD_INVALID_TOKEN_ERROR') + throw new Error('Should never come here') + + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/userdetails/userPut.ts b/src/recipe/dashboard/api/userdetails/userPut.ts new file mode 100644 index 000000000..e25542206 --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userPut.ts @@ -0,0 +1,442 @@ +import { APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import EmailPasswordRecipe from '../../../emailpassword/recipe' +import ThirdPartyEmailPasswordRecipe from '../../../thirdpartyemailpassword/recipe' +import PasswordlessRecipe from '../../../passwordless/recipe' +import ThirdPartyPasswordlessRecipe from '../../../thirdpartypasswordless/recipe' +import EmailPassword from '../../../emailpassword' +import Passwordless from '../../../passwordless' +import ThirdPartyEmailPassword from '../../../thirdpartyemailpassword' +import ThirdPartyPasswordless from '../../../thirdpartypasswordless' +import { getUserForRecipeId, isValidRecipeId } from '../../utils' +import UserMetadataRecipe from '../../../usermetadata/recipe' +import UserMetadata from '../../../usermetadata' +import { FORM_FIELD_EMAIL_ID } from '../../../emailpassword/constants' +import { defaultValidateEmail, defaultValidatePhoneNumber } from '../../../passwordless/utils' + +type Response = + | { + status: 'OK' + } + | { + status: 'EMAIL_ALREADY_EXISTS_ERROR' + } + | { + status: 'INVALID_EMAIL_ERROR' + error: string + } + | { + status: 'PHONE_ALREADY_EXISTS_ERROR' + } + | { + status: 'INVALID_PHONE_ERROR' + error: string + } + +const updateEmailForRecipeId = async ( + recipeId: 'emailpassword' | 'thirdparty' | 'passwordless' | 'thirdpartyemailpassword' | 'thirdpartypasswordless', + userId: string, + email: string, +): Promise< + | { + status: 'OK' + } + | { + status: 'INVALID_EMAIL_ERROR' + error: string + } + | { + status: 'EMAIL_ALREADY_EXISTS_ERROR' + } +> => { + if (recipeId === 'emailpassword') { + const emailFormFields = EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( + field => field.id === FORM_FIELD_EMAIL_ID, + ) + + const validationError = await emailFormFields[0].validate(email) + + if (validationError !== undefined) { + return { + status: 'INVALID_EMAIL_ERROR', + error: validationError, + } + } + + const emailUpdateResponse = await EmailPassword.updateEmailOrPassword({ + userId, + email, + }) + + if (emailUpdateResponse.status === 'EMAIL_ALREADY_EXISTS_ERROR') { + return { + status: 'EMAIL_ALREADY_EXISTS_ERROR', + } + } + + return { + status: 'OK', + } + } + + if (recipeId === 'thirdpartyemailpassword') { + const emailFormFields = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( + field => field.id === FORM_FIELD_EMAIL_ID, + ) + + const validationError = await emailFormFields[0].validate(email) + + if (validationError !== undefined) { + return { + status: 'INVALID_EMAIL_ERROR', + error: validationError, + } + } + + const emailUpdateResponse = await ThirdPartyEmailPassword.updateEmailOrPassword({ + userId, + email, + }) + + if (emailUpdateResponse.status === 'EMAIL_ALREADY_EXISTS_ERROR') { + return { + status: 'EMAIL_ALREADY_EXISTS_ERROR', + } + } + + if (emailUpdateResponse.status === 'UNKNOWN_USER_ID_ERROR') + throw new Error('Should never come here') + + return { + status: 'OK', + } + } + + if (recipeId === 'passwordless') { + let isValidEmail = true + let validationError = '' + + const passwordlessConfig = PasswordlessRecipe.getInstanceOrThrowError().config + + if (passwordlessConfig.contactMethod === 'PHONE') { + const validationResult = await defaultValidateEmail(email) + + if (validationResult !== undefined) { + isValidEmail = false + validationError = validationResult + } + } + else { + const validationResult = await passwordlessConfig.validateEmailAddress(email) + + if (validationResult !== undefined) { + isValidEmail = false + validationError = validationResult + } + } + + if (!isValidEmail) { + return { + status: 'INVALID_EMAIL_ERROR', + error: validationError, + } + } + + const updateResult = await Passwordless.updateUser({ + userId, + email, + }) + + if (updateResult.status === 'UNKNOWN_USER_ID_ERROR') + throw new Error('Should never come here') + + if (updateResult.status === 'EMAIL_ALREADY_EXISTS_ERROR') { + return { + status: 'EMAIL_ALREADY_EXISTS_ERROR', + } + } + + return { + status: 'OK', + } + } + + if (recipeId === 'thirdpartypasswordless') { + let isValidEmail = true + let validationError = '' + + const passwordlessConfig = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().passwordlessRecipe.config + + if (passwordlessConfig.contactMethod === 'PHONE') { + const validationResult = await defaultValidateEmail(email) + + if (validationResult !== undefined) { + isValidEmail = false + validationError = validationResult + } + } + else { + const validationResult = await passwordlessConfig.validateEmailAddress(email) + + if (validationResult !== undefined) { + isValidEmail = false + validationError = validationResult + } + } + + if (!isValidEmail) { + return { + status: 'INVALID_EMAIL_ERROR', + error: validationError, + } + } + + const updateResult = await ThirdPartyPasswordless.updatePasswordlessUser({ + userId, + email, + }) + + if (updateResult.status === 'UNKNOWN_USER_ID_ERROR') + throw new Error('Should never come here') + + if (updateResult.status === 'EMAIL_ALREADY_EXISTS_ERROR') { + return { + status: 'EMAIL_ALREADY_EXISTS_ERROR', + } + } + + return { + status: 'OK', + } + } + + /** + * If it comes here then the user is a third party user in which case the UI should not have allowed this + */ + throw new Error('Should never come here') +} + +const updatePhoneForRecipeId = async ( + recipeId: 'emailpassword' | 'thirdparty' | 'passwordless' | 'thirdpartyemailpassword' | 'thirdpartypasswordless', + userId: string, + phone: string, +): Promise< + | { + status: 'OK' + } + | { + status: 'INVALID_PHONE_ERROR' + error: string + } + | { + status: 'PHONE_ALREADY_EXISTS_ERROR' + } +> => { + if (recipeId === 'passwordless') { + let isValidPhone = true + let validationError = '' + + const passwordlessConfig = PasswordlessRecipe.getInstanceOrThrowError().config + + if (passwordlessConfig.contactMethod === 'EMAIL') { + const validationResult = await defaultValidatePhoneNumber(phone) + + if (validationResult !== undefined) { + isValidPhone = false + validationError = validationResult + } + } + else { + const validationResult = await passwordlessConfig.validatePhoneNumber(phone) + + if (validationResult !== undefined) { + isValidPhone = false + validationError = validationResult + } + } + + if (!isValidPhone) { + return { + status: 'INVALID_PHONE_ERROR', + error: validationError, + } + } + + const updateResult = await Passwordless.updateUser({ + userId, + phoneNumber: phone, + }) + + if (updateResult.status === 'UNKNOWN_USER_ID_ERROR') + throw new Error('Should never come here') + + if (updateResult.status === 'PHONE_NUMBER_ALREADY_EXISTS_ERROR') { + return { + status: 'PHONE_ALREADY_EXISTS_ERROR', + } + } + + return { + status: 'OK', + } + } + + if (recipeId === 'thirdpartypasswordless') { + let isValidPhone = true + let validationError = '' + + const passwordlessConfig = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().passwordlessRecipe.config + + if (passwordlessConfig.contactMethod === 'EMAIL') { + const validationResult = await defaultValidatePhoneNumber(phone) + + if (validationResult !== undefined) { + isValidPhone = false + validationError = validationResult + } + } + else { + const validationResult = await passwordlessConfig.validatePhoneNumber(phone) + + if (validationResult !== undefined) { + isValidPhone = false + validationError = validationResult + } + } + + if (!isValidPhone) { + return { + status: 'INVALID_PHONE_ERROR', + error: validationError, + } + } + + const updateResult = await ThirdPartyPasswordless.updatePasswordlessUser({ + userId, + phoneNumber: phone, + }) + + if (updateResult.status === 'UNKNOWN_USER_ID_ERROR') + throw new Error('Should never come here') + + if (updateResult.status === 'PHONE_NUMBER_ALREADY_EXISTS_ERROR') { + return { + status: 'PHONE_ALREADY_EXISTS_ERROR', + } + } + + return { + status: 'OK', + } + } + + /** + * If it comes here then the user is a not a passwordless user in which case the UI should not have allowed this + */ + throw new Error('Should never come here') +} + +export const userPut = async (_: APIInterface, options: APIOptions): Promise => { + const requestBody = await options.req.getJSONBody() + const userId = requestBody.userId + const recipeId = requestBody.recipeId + const firstName = requestBody.firstName + const lastName = requestBody.lastName + const email = requestBody.email + const phone = requestBody.phone + + if (userId === undefined || typeof userId !== 'string') { + throw new STError({ + message: 'Required parameter \'userId\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (recipeId === undefined || typeof recipeId !== 'string') { + throw new STError({ + message: 'Required parameter \'recipeId\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (!isValidRecipeId(recipeId)) { + throw new STError({ + message: 'Invalid recipe id', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (firstName === undefined || typeof firstName !== 'string') { + throw new STError({ + message: 'Required parameter \'firstName\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (lastName === undefined || typeof lastName !== 'string') { + throw new STError({ + message: 'Required parameter \'lastName\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (email === undefined || typeof email !== 'string') { + throw new STError({ + message: 'Required parameter \'email\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + if (phone === undefined || typeof phone !== 'string') { + throw new STError({ + message: 'Required parameter \'phone\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + const userResponse = await getUserForRecipeId(userId, recipeId) + + if (userResponse.user === undefined || userResponse.recipe === undefined) + throw new Error('Should never come here') + + if (firstName.trim() !== '' || lastName.trim() !== '') { + let isRecipeInitialised = false + try { + UserMetadataRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { + // no op + } + + if (isRecipeInitialised) { + const metaDataUpdate: any = {} + + if (firstName.trim() !== '') + metaDataUpdate.first_name = firstName.trim() + + if (lastName.trim() !== '') + metaDataUpdate.last_name = lastName.trim() + + await UserMetadata.updateUserMetadata(userId, metaDataUpdate) + } + } + + if (email.trim() !== '') { + const emailUpdateResponse = await updateEmailForRecipeId(userResponse.recipe, userId, email.trim()) + + if (emailUpdateResponse.status !== 'OK') + return emailUpdateResponse + } + + if (phone.trim() !== '') { + const phoneUpdateResponse = await updatePhoneForRecipeId(userResponse.recipe, userId, phone.trim()) + + if (phoneUpdateResponse.status !== 'OK') + return phoneUpdateResponse + } + + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/userdetails/userSessionsGet.ts b/src/recipe/dashboard/api/userdetails/userSessionsGet.ts new file mode 100644 index 000000000..d4e575da4 --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userSessionsGet.ts @@ -0,0 +1,59 @@ +import { APIFunction, APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import Session from '../../../session' + +interface SessionType { + sessionData: any + accessTokenPayload: any + userId: string + expiry: number + timeCreated: number + sessionHandle: string +} + +interface Response { + status: 'OK' + sessions: SessionType[] +} + +export const userSessionsGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { + const userId = options.req.getKeyValueFromQuery('userId') + + if (userId === undefined) { + throw new STError({ + message: 'Missing required parameter \'userId\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + const response = await Session.getAllSessionHandlesForUser(userId) + + const sessions: SessionType[] = [] + const sessionInfoPromises: Promise[] = [] + + for (let i = 0; i < response.length; i++) { + sessionInfoPromises.push( + new Promise((resolve, reject) => { + try { + Session.getSessionInformation(response[i]).then((session) => { + if (session !== undefined) + sessions[i] = session + resolve() + }).catch((err) => { + reject(err) + }) + } + catch (e) { + reject(e) + } + }), + ) + } + + await Promise.all(sessionInfoPromises) + + return { + status: 'OK', + sessions, + } +} diff --git a/src/recipe/dashboard/api/userdetails/userSessionsPost.ts b/src/recipe/dashboard/api/userdetails/userSessionsPost.ts new file mode 100644 index 000000000..315f0a037 --- /dev/null +++ b/src/recipe/dashboard/api/userdetails/userSessionsPost.ts @@ -0,0 +1,24 @@ +import { APIInterface, APIOptions } from '../../types' +import STError from '../../../../error' +import Session from '../../../session' + +interface Response { + status: 'OK' +} + +export const userSessionsPost = async (_: APIInterface, options: APIOptions): Promise => { + const requestBody = await options.req.getJSONBody() + const sessionHandles = requestBody.sessionHandles + + if (sessionHandles === undefined || !Array.isArray(sessionHandles)) { + throw new STError({ + message: 'Required parameter \'sessionHandles\' is missing or has an invalid type', + type: STError.BAD_INPUT_ERROR, + }) + } + + await Session.revokeMultipleSessions(sessionHandles) + return { + status: 'OK', + } +} diff --git a/src/recipe/dashboard/api/usersCountGet.ts b/src/recipe/dashboard/api/usersCountGet.ts new file mode 100644 index 000000000..112ecee1f --- /dev/null +++ b/src/recipe/dashboard/api/usersCountGet.ts @@ -0,0 +1,31 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { APIInterface, APIOptions } from '../types' +import SuperTokens from '../../../supertokens' + +export interface Response { + status: 'OK' + count: number +} + +export default async function usersCountGet(_: APIInterface, __: APIOptions): Promise { + const count = await SuperTokens.getInstanceOrThrowError().getUserCount() + + return { + status: 'OK', + count, + } +} diff --git a/src/recipe/dashboard/api/usersGet.ts b/src/recipe/dashboard/api/usersGet.ts new file mode 100644 index 000000000..5f67e2f15 --- /dev/null +++ b/src/recipe/dashboard/api/usersGet.ts @@ -0,0 +1,183 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { APIInterface, APIOptions } from '../types' +import STError from '../../../error' +import SuperTokens from '../../../supertokens' +import UserMetaDataRecipe from '../../usermetadata/recipe' +import UserMetaData from '../../usermetadata' + +export interface Response { + status: 'OK' + nextPaginationToken?: string + users: { + recipeId: string + user: { + id: string + timeJoined: number + firstName?: string + lastName?: string + } & ( + | { + email: string + } + | { + email: string + thirdParty: { + id: string + userId: string + } + } + | { + email?: string + phoneNumber?: string + } + ) + }[] +} + +export default async function usersGet(_: APIInterface, options: APIOptions): Promise { + const req = options.req + const limit = options.req.getKeyValueFromQuery('limit') + + if (limit === undefined) { + throw new STError({ + message: 'Missing required parameter \'limit\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + let timeJoinedOrder = req.getKeyValueFromQuery('timeJoinedOrder') + + if (timeJoinedOrder === undefined) + timeJoinedOrder = 'DESC' + + if (timeJoinedOrder !== 'ASC' && timeJoinedOrder !== 'DESC') { + throw new STError({ + message: 'Invalid value recieved for \'timeJoinedOrder\'', + type: STError.BAD_INPUT_ERROR, + }) + } + + const paginationToken = options.req.getKeyValueFromQuery('paginationToken') + const query = getSearchParamsFromURL(options.req.getOriginalURL()) + let usersResponse = await SuperTokens.getInstanceOrThrowError().getUsers({ + query, + timeJoinedOrder, + limit: parseInt(limit), + paginationToken, + }) + + // If the UserMetaData recipe has been initialised, fetch first and last name + try { + UserMetaDataRecipe.getInstanceOrThrowError() + } + catch (e) { + // Recipe has not been initialised, return without first name and last name + return { + status: 'OK', + users: usersResponse.users, + nextPaginationToken: usersResponse.nextPaginationToken, + } + } + + const updatedUsersArray: { + recipeId: string + user: any + }[] = [] + const metaDataFetchPromises: (() => Promise)[] = [] + + for (let i = 0; i < usersResponse.users.length; i++) { + const userObj = usersResponse.users[i] + metaDataFetchPromises.push( + (): Promise => + // TODO: check async + // eslint-disable-next-line no-async-promise-executor + new Promise(async (resolve, reject) => { + try { + const userMetaDataResponse = await UserMetaData.getUserMetadata(userObj.user.id) + const { first_name, last_name } = userMetaDataResponse.metadata + + updatedUsersArray[i] = { + recipeId: userObj.recipeId, + user: { + ...userObj.user, + firstName: first_name, + lastName: last_name, + }, + } + resolve(true) + } + catch (e) { + // Something went wrong when fetching user meta data + reject(e) + } + }), + ) + } + + let promiseArrayStartPosition = 0 + const batchSize = 5 + + while (promiseArrayStartPosition < metaDataFetchPromises.length) { + /** + * We want to query only 5 in parallel at a time + * + * First we check if the the array has enough elements to iterate + * promiseArrayStartPosition + 4 (5 elements including current) + */ + let promiseArrayEndPosition = promiseArrayStartPosition + (batchSize - 1) + + // If the end position is higher than the arrays length, we need to adjust it + if (promiseArrayEndPosition >= metaDataFetchPromises.length) { + /** + * For example if the array has 7 elements [A, B, C, D, E, F, G], when you run + * the second batch [startPosition = 5], this will result in promiseArrayEndPosition + * to be equal to 6 [5 + ((7 - 1) - 5)] and will then iterate over indexes [5] and [6] + */ + promiseArrayEndPosition + = promiseArrayStartPosition + (metaDataFetchPromises.length - 1 - promiseArrayStartPosition) + } + + const promisesToCall: (() => Promise)[] = [] + + for (let j = promiseArrayStartPosition; j <= promiseArrayEndPosition; j++) + promisesToCall.push(metaDataFetchPromises[j]) + + await Promise.all(promisesToCall.map(p => p())) + promiseArrayStartPosition += batchSize + } + + usersResponse = { + ...usersResponse, + users: updatedUsersArray, + } + + return { + status: 'OK', + users: usersResponse.users, + nextPaginationToken: usersResponse.nextPaginationToken, + } +} + +export function getSearchParamsFromURL(path: string): { [key: string]: string } { + const URLObject = new URL(`https://exmaple.com${path}`) + const params = new URLSearchParams(URLObject.search) + const searchQuery: { [key: string]: string } = {} + for (const [key, value] of params) { + if (!['limit', 'timeJoinedOrder', 'paginationToken'].includes(key)) + searchQuery[key] = value + } + return searchQuery +} diff --git a/src/recipe/dashboard/api/validateKey.ts b/src/recipe/dashboard/api/validateKey.ts new file mode 100644 index 000000000..69f38d115 --- /dev/null +++ b/src/recipe/dashboard/api/validateKey.ts @@ -0,0 +1,33 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI } from '../../../utils' +import { APIInterface, APIOptions } from '../types' +import { sendUnauthorisedAccess, validateApiKey } from '../utils' + +export default async function validateKey(_: APIInterface, options: APIOptions): Promise { + const input = { req: options.req, config: options.config, userContext: makeDefaultUserContextFromAPI(options.req) } + + if (await validateApiKey(input)) { + options.res.sendJSONResponse({ + status: 'OK', + }) + } + else { + sendUnauthorisedAccess(options.res) + } + + return true +} diff --git a/src/recipe/dashboard/constants.ts b/src/recipe/dashboard/constants.ts new file mode 100644 index 000000000..391d9f339 --- /dev/null +++ b/src/recipe/dashboard/constants.ts @@ -0,0 +1,29 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const DASHBOARD_API = '/dashboard' +export const SIGN_IN_API = '/api/signin' +export const SIGN_OUT_API = '/api/signout' +export const VALIDATE_KEY_API = '/api/key/validate' +export const USERS_LIST_GET_API = '/api/users' +export const USERS_COUNT_API = '/api/users/count' +export const USER_API = '/api/user' +export const USER_EMAIL_VERIFY_API = '/api/user/email/verify' +export const USER_METADATA_API = '/api/user/metadata' +export const USER_SESSIONS_API = '/api/user/sessions' +export const USER_PASSWORD_API = '/api/user/password' +export const USER_EMAIL_VERIFY_TOKEN_API = '/api/user/email/verify/token' +export const SEARCH_TAGS_API = '/api/search/tags' +export const DASHBOARD_ANALYTICS_API = '/api/analytics' diff --git a/src/recipe/dashboard/index.ts b/src/recipe/dashboard/index.ts new file mode 100644 index 000000000..0480bcdc0 --- /dev/null +++ b/src/recipe/dashboard/index.ts @@ -0,0 +1,24 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import Recipe from './recipe' +import { APIInterface, APIOptions, RecipeInterface } from './types' + +export default class Wrapper { + static init = Recipe.init +} + +export const init = Wrapper.init + +export type { RecipeInterface, APIOptions, APIInterface } diff --git a/src/recipe/dashboard/recipe.ts b/src/recipe/dashboard/recipe.ts new file mode 100644 index 000000000..06995036a --- /dev/null +++ b/src/recipe/dashboard/recipe.ts @@ -0,0 +1,362 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import NormalisedURLPath from '../../normalisedURLPath' +import { BaseRequest, BaseResponse } from '../../framework' +import error from '../../error' +import { APIFunction, APIInterface, APIOptions, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import RecipeImplementation from './recipeImplementation' +import APIImplementation from './api/implementation' +import { getApiIdIfMatched, getApiPathWithDashboardBase, isApiPath, validateAndNormaliseUserInput } from './utils' +import { + DASHBOARD_ANALYTICS_API, + DASHBOARD_API, + SEARCH_TAGS_API, + SIGN_IN_API, + SIGN_OUT_API, + USERS_COUNT_API, + USERS_LIST_GET_API, + USER_API, + USER_EMAIL_VERIFY_API, + USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, + USER_PASSWORD_API, + USER_SESSIONS_API, + VALIDATE_KEY_API, +} from './constants' +import dashboard from './api/dashboard' +import validateKey from './api/validateKey' +import apiKeyProtector from './api/apiKeyProtector' +import usersGet from './api/usersGet' +import usersCountGet from './api/usersCountGet' +import { userGet } from './api/userdetails/userGet' +import { userEmailverifyGet } from './api/userdetails/userEmailVerifyGet' +import { userMetaDataGet } from './api/userdetails/userMetadataGet' +import { userSessionsGet } from './api/userdetails/userSessionsGet' +import { userDelete } from './api/userdetails/userDelete' +import { userEmailVerifyPut } from './api/userdetails/userEmailVerifyPut' +import { userMetadataPut } from './api/userdetails/userMetadataPut' +import { userPasswordPut } from './api/userdetails/userPasswordPut' +import { userPut } from './api/userdetails/userPut' +import { userEmailVerifyTokenPost } from './api/userdetails/userEmailVerifyTokenPost' +import { userSessionsPost } from './api/userdetails/userSessionsPost' +import signIn from './api/signIn' +import signOut from './api/signOut' +import { getSearchTags } from './api/search/tagsGet' +import analyticsPost from './api/analytics' + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined + static RECIPE_ID = 'dashboard' + + config: TypeNormalisedInput + + recipeInterfaceImpl: RecipeInterface + + apiImpl: APIInterface + + isInServerlessEnv: boolean + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo) + + this.config = validateAndNormaliseUserInput(config) + this.isInServerlessEnv = isInServerlessEnv + + { + const builder = new OverrideableBuilder(RecipeImplementation()) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config) + return Recipe.instance + } + else { + throw new Error('Dashboard recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + // abstract instance functions below............... + + getAPIsHandled = (): APIHandled[] => { + /** + * Normally this array is used by the SDK to decide whether or not the recipe + * handles a specific API path and method and then returns the ID. + * + * For the dashboard recipe this logic is fully custom and handled inside the + * `returnAPIIdIfCanHandleRequest` method of this class. + * + * For most frameworks this array is redundant because the `returnAPIIdIfCanHandleRequest` is used. + * But for frameworks such as Hapi that require all APIs to be declared up front, this array is used + * to make sure that the framework does not return a 404 + */ + return [ + { + id: DASHBOARD_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(DASHBOARD_API)), + disabled: false, + method: 'get', + }, + { + id: SIGN_IN_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(SIGN_IN_API)), + disabled: false, + method: 'post', + }, + { + id: VALIDATE_KEY_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(VALIDATE_KEY_API)), + disabled: false, + method: 'post', + }, + { + id: SIGN_OUT_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(SIGN_OUT_API)), + disabled: false, + method: 'post', + }, + { + id: USERS_LIST_GET_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USERS_LIST_GET_API)), + disabled: false, + method: 'get', + }, + { + id: USERS_COUNT_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USERS_COUNT_API)), + disabled: false, + method: 'get', + }, + { + id: USER_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_API)), + disabled: false, + method: 'get', + }, + { + id: USER_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_API)), + disabled: false, + method: 'post', + }, + { + id: USER_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_API)), + disabled: false, + method: 'delete', + }, + { + id: USER_EMAIL_VERIFY_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_EMAIL_VERIFY_API)), + disabled: false, + method: 'get', + }, + { + id: USER_EMAIL_VERIFY_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_EMAIL_VERIFY_API)), + disabled: false, + method: 'put', + }, + { + id: USER_METADATA_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_METADATA_API)), + disabled: false, + method: 'get', + }, + { + id: USER_METADATA_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_METADATA_API)), + disabled: false, + method: 'put', + }, + { + id: USER_SESSIONS_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_SESSIONS_API)), + disabled: false, + method: 'get', + }, + { + id: USER_SESSIONS_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_SESSIONS_API)), + disabled: false, + method: 'post', + }, + { + id: USER_PASSWORD_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_PASSWORD_API)), + disabled: false, + method: 'put', + }, + { + id: USER_EMAIL_VERIFY_TOKEN_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(USER_EMAIL_VERIFY_TOKEN_API)), + disabled: false, + method: 'post', + }, + { + id: SEARCH_TAGS_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(SEARCH_TAGS_API)), + disabled: false, + method: 'get', + }, + { + id: DASHBOARD_ANALYTICS_API, + pathWithoutApiBasePath: new NormalisedURLPath(getApiPathWithDashboardBase(DASHBOARD_ANALYTICS_API)), + disabled: false, + method: 'post', + }, + ] + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + __: NormalisedURLPath, + ___: HTTPMethod, + ): Promise => { + const options: APIOptions = { + config: this.config, + recipeId: this.getRecipeId(), + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + isInServerlessEnv: this.isInServerlessEnv, + appInfo: this.getAppInfo(), + } + + // For these APIs we dont need API key validation + if (id === DASHBOARD_API) + return await dashboard(this.apiImpl, options) + + if (id === SIGN_IN_API) + return await signIn(this.apiImpl, options) + + if (id === VALIDATE_KEY_API) + return await validateKey(this.apiImpl, options) + + // Do API key validation for the remaining APIs + let apiFunction: APIFunction | undefined + + if (id === USERS_LIST_GET_API) { + apiFunction = usersGet + } + else if (id === USERS_COUNT_API) { + apiFunction = usersCountGet + } + else if (id === USER_API) { + if (req.getMethod() === 'get') + apiFunction = userGet + + if (req.getMethod() === 'delete') + apiFunction = userDelete + + if (req.getMethod() === 'put') + apiFunction = userPut + } + else if (id === USER_EMAIL_VERIFY_API) { + if (req.getMethod() === 'get') + apiFunction = userEmailverifyGet + + if (req.getMethod() === 'put') + apiFunction = userEmailVerifyPut + } + else if (id === USER_METADATA_API) { + if (req.getMethod() === 'get') + apiFunction = userMetaDataGet + + if (req.getMethod() === 'put') + apiFunction = userMetadataPut + } + else if (id === USER_SESSIONS_API) { + if (req.getMethod() === 'get') + apiFunction = userSessionsGet + + if (req.getMethod() === 'post') + apiFunction = userSessionsPost + } + else if (id === USER_PASSWORD_API) { + apiFunction = userPasswordPut + } + else if (id === USER_EMAIL_VERIFY_TOKEN_API) { + apiFunction = userEmailVerifyTokenPost + } + else if (id === SEARCH_TAGS_API) { + apiFunction = getSearchTags + } + else if (id === SIGN_OUT_API) { + apiFunction = signOut + } + else if (id === DASHBOARD_ANALYTICS_API && req.getMethod() === 'post') { + apiFunction = analyticsPost + } + + // If the id doesnt match any APIs return false + if (apiFunction === undefined) + return false + + return await apiKeyProtector(this.apiImpl, options, apiFunction) + } + + handleError = async (err: error, _: BaseRequest, __: BaseResponse): Promise => { + throw err + } + + getAllCORSHeaders = (): string[] => { + return [] + } + + isErrorFromThisRecipe = (err: any): err is error => { + return error.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } + + returnAPIIdIfCanHandleRequest = (path: NormalisedURLPath, method: HTTPMethod): string | undefined => { + const dashboardBundlePath = this.getAppInfo().apiBasePath.appendPath(new NormalisedURLPath(DASHBOARD_API)) + + if (isApiPath(path, this.getAppInfo())) + return getApiIdIfMatched(path, method) + + if (path.startsWith(dashboardBundlePath)) + return DASHBOARD_API + + return undefined + } +} diff --git a/src/recipe/dashboard/recipeImplementation.ts b/src/recipe/dashboard/recipeImplementation.ts new file mode 100644 index 000000000..16fb9bceb --- /dev/null +++ b/src/recipe/dashboard/recipeImplementation.ts @@ -0,0 +1,44 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import { dashboardVersion } from '../../version' +import { RecipeInterface } from './types' +import { validateApiKey } from './utils' + +export default function getRecipeImplementation(): RecipeInterface { + return { + async getDashboardBundleLocation() { + return `https://cdn.jsdelivr.net/gh/supertokens/dashboard@v${dashboardVersion}/build/` + }, + async shouldAllowAccess(input) { + // For cases where we're not using the API key, the JWT is being used; we allow their access by default + if (!input.config.apiKey) { + // make the check for the API endpoint here with querier + const querier = Querier.getNewInstanceOrThrowError(undefined) + const authHeaderValue = input.req.getHeaderValue('authorization')?.split(' ')[1] + const sessionVerificationResponse = await querier.sendPostRequest( + new NormalisedURLPath('/recipe/dashboard/session/verify'), + { + sessionId: authHeaderValue, + }, + ) + return sessionVerificationResponse.status === 'OK' + } + return await validateApiKey(input) + }, + } +} diff --git a/src/recipe/dashboard/types.ts b/src/recipe/dashboard/types.ts new file mode 100644 index 000000000..2d995298e --- /dev/null +++ b/src/recipe/dashboard/types.ts @@ -0,0 +1,91 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { NormalisedAppinfo } from '../../types' + +export interface TypeInput { + apiKey?: string + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeNormalisedInput { + apiKey?: string + authMode: AuthMode + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface RecipeInterface { + getDashboardBundleLocation(input: { userContext: any }): Promise + shouldAllowAccess(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }): Promise +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + config: TypeNormalisedInput + recipeId: string + req: BaseRequest + res: BaseResponse + isInServerlessEnv: boolean + appInfo: NormalisedAppinfo +} + +export interface APIInterface { + dashboardGET: undefined | ((input: { options: APIOptions; userContext: any }) => Promise) +} + +export type APIFunction = (apiImplementation: APIInterface, options: APIOptions) => Promise + +export type RecipeIdForUser = 'emailpassword' | 'thirdparty' | 'passwordless' + +export type AuthMode = 'api-key' | 'email-password' + +interface CommonUserInformation { + id: string + timeJoined: number + firstName: string + lastName: string +} + +export type EmailPasswordUser = CommonUserInformation & { + email: string +} + +export type ThirdPartyUser = CommonUserInformation & { + email: string + thirdParty: { + id: string + userId: string + } +} + +export type PasswordlessUser = CommonUserInformation & { + email?: string + phone?: string +} diff --git a/src/recipe/dashboard/utils.ts b/src/recipe/dashboard/utils.ts new file mode 100644 index 000000000..533f591a9 --- /dev/null +++ b/src/recipe/dashboard/utils.ts @@ -0,0 +1,376 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { BaseRequest, BaseResponse } from '../../framework' +import NormalisedURLPath from '../../normalisedURLPath' +import { HTTPMethod, NormalisedAppinfo } from '../../types' +import { sendNon200ResponseWithMessage } from '../../utils' +import EmailPasswordRecipe from '../emailpassword/recipe' +import ThirdPartyRecipe from '../thirdparty/recipe' +import PasswordlessRecipe from '../passwordless/recipe' +import EmailPassword from '../emailpassword' +import ThirdParty from '../thirdparty' +import Passwordless from '../passwordless' +import ThirdPartyEmailPassword from '../thirdpartyemailpassword' +import ThirdPartyEmailPasswordRecipe from '../thirdpartyemailpassword/recipe' +import ThirdPartyPasswordless from '../thirdpartypasswordless' +import ThirdPartyPasswordlessRecipe from '../thirdpartypasswordless/recipe' +import { + APIInterface, + EmailPasswordUser, + PasswordlessUser, + RecipeIdForUser, + RecipeInterface, + ThirdPartyUser, + TypeInput, + TypeNormalisedInput, +} from './types' +import { + DASHBOARD_ANALYTICS_API, + DASHBOARD_API, + SEARCH_TAGS_API, + SIGN_IN_API, + SIGN_OUT_API, + USERS_COUNT_API, + USERS_LIST_GET_API, + USER_API, + USER_EMAIL_VERIFY_API, + USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, + USER_PASSWORD_API, + USER_SESSIONS_API, + VALIDATE_KEY_API, +} from './constants' + +export function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput { + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...(config === undefined ? {} : config.override), + } + + return { + override, + authMode: (config !== undefined && config.apiKey) ? 'api-key' : 'email-password', + } +} + +export function isApiPath(path: NormalisedURLPath, appInfo: NormalisedAppinfo): boolean { + const dashboardRecipeBasePath = appInfo.apiBasePath.appendPath(new NormalisedURLPath(DASHBOARD_API)) + if (!path.startsWith(dashboardRecipeBasePath)) + return false + + let pathWithoutDashboardPath = path.getAsStringDangerous().split(DASHBOARD_API)[1] + + if (pathWithoutDashboardPath.charAt(0) === '/') + pathWithoutDashboardPath = pathWithoutDashboardPath.substring(1, pathWithoutDashboardPath.length) + + if (pathWithoutDashboardPath.split('/')[0] === 'api') + return true + + return false +} + +export function getApiIdIfMatched(path: NormalisedURLPath, method: HTTPMethod): string | undefined { + if (path.getAsStringDangerous().endsWith(VALIDATE_KEY_API) && method === 'post') + return VALIDATE_KEY_API + + if (path.getAsStringDangerous().endsWith(SIGN_IN_API) && method === 'post') + return SIGN_IN_API + + if (path.getAsStringDangerous().endsWith(SIGN_OUT_API) && method === 'post') + return SIGN_OUT_API + + if (path.getAsStringDangerous().endsWith(USERS_LIST_GET_API) && method === 'get') + return USERS_LIST_GET_API + + if (path.getAsStringDangerous().endsWith(USERS_COUNT_API) && method === 'get') + return USERS_COUNT_API + + if (path.getAsStringDangerous().endsWith(USER_API)) { + if (method === 'get' || method === 'delete' || method === 'put') + return USER_API + } + + if (path.getAsStringDangerous().endsWith(USER_EMAIL_VERIFY_API)) { + if (method === 'get' || method === 'put') + return USER_EMAIL_VERIFY_API + } + + if (path.getAsStringDangerous().endsWith(USER_METADATA_API)) { + if (method === 'get' || method === 'put') + return USER_METADATA_API + } + + if (path.getAsStringDangerous().endsWith(USER_SESSIONS_API)) { + if (method === 'get' || method === 'post') + return USER_SESSIONS_API + } + + if (path.getAsStringDangerous().endsWith(USER_PASSWORD_API) && method === 'put') + return USER_PASSWORD_API + + if (path.getAsStringDangerous().endsWith(USER_EMAIL_VERIFY_TOKEN_API) && method === 'post') + return USER_EMAIL_VERIFY_TOKEN_API + + if (path.getAsStringDangerous().endsWith(USER_PASSWORD_API) && method === 'put') + return USER_PASSWORD_API + + if (path.getAsStringDangerous().endsWith(SEARCH_TAGS_API) && method === 'get') + return SEARCH_TAGS_API + + if (path.getAsStringDangerous().endsWith(DASHBOARD_ANALYTICS_API) && method === 'post') + return DASHBOARD_ANALYTICS_API + + return undefined +} + +export function sendUnauthorisedAccess(res: BaseResponse) { + sendNon200ResponseWithMessage(res, 'Unauthorised access', 401) +} + +export function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser { + return recipeId === 'emailpassword' || recipeId === 'thirdparty' || recipeId === 'passwordless' +} + +export async function getUserForRecipeId( + userId: string, + recipeId: string, +): Promise<{ + user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined + recipe: + | 'emailpassword' + | 'thirdparty' + | 'passwordless' + | 'thirdpartyemailpassword' + | 'thirdpartypasswordless' + | undefined +}> { + let user: EmailPasswordUser | ThirdPartyUser | PasswordlessUser | undefined + let recipe: + | 'emailpassword' + | 'thirdparty' + | 'passwordless' + | 'thirdpartyemailpassword' + | 'thirdpartypasswordless' + | undefined + + if (recipeId === EmailPasswordRecipe.RECIPE_ID) { + try { + const userResponse = await EmailPassword.getUserById(userId) + + if (userResponse !== undefined) { + user = { + ...userResponse, + firstName: '', + lastName: '', + } + recipe = 'emailpassword' + } + } + catch (e) { + // No - op + } + + if (user === undefined) { + try { + const userResponse = await ThirdPartyEmailPassword.getUserById(userId) + + if (userResponse !== undefined) { + user = { + ...userResponse, + firstName: '', + lastName: '', + } + recipe = 'thirdpartyemailpassword' + } + } + catch (e) { + // No - op + } + } + } + else if (recipeId === ThirdPartyRecipe.RECIPE_ID) { + try { + const userResponse = await ThirdParty.getUserById(userId) + + if (userResponse !== undefined) { + user = { + ...userResponse, + firstName: '', + lastName: '', + } + recipe = 'thirdparty' + } + } + catch (e) { + // No - op + } + + if (user === undefined) { + try { + const userResponse = await ThirdPartyEmailPassword.getUserById(userId) + + if (userResponse !== undefined) { + user = { + ...userResponse, + firstName: '', + lastName: '', + } + recipe = 'thirdpartyemailpassword' + } + } + catch (e) { + // No - op + } + } + + if (user === undefined) { + try { + const userResponse = await ThirdPartyPasswordless.getUserById(userId) + + if (userResponse !== undefined) { + user = { + ...userResponse, + firstName: '', + lastName: '', + } + recipe = 'thirdpartypasswordless' + } + } + catch (e) { + // No - op + } + } + } + else if (recipeId === PasswordlessRecipe.RECIPE_ID) { + try { + const userResponse = await Passwordless.getUserById({ + userId, + }) + + if (userResponse !== undefined) { + user = { + ...userResponse, + firstName: '', + lastName: '', + } + recipe = 'passwordless' + } + } + catch (e) { + // No - op + } + + if (user === undefined) { + try { + const userResponse = await ThirdPartyPasswordless.getUserById(userId) + + if (userResponse !== undefined) { + user = { + ...userResponse, + firstName: '', + lastName: '', + } + recipe = 'thirdpartypasswordless' + } + } + catch (e) { + // No - op + } + } + } + + return { + user, + recipe, + } +} + +export function isRecipeInitialised(recipeId: RecipeIdForUser): boolean { + let isRecipeInitialised = false + + if (recipeId === 'emailpassword') { + try { + EmailPasswordRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { } + + if (!isRecipeInitialised) { + try { + ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { } + } + } + else if (recipeId === 'passwordless') { + try { + PasswordlessRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { } + + if (!isRecipeInitialised) { + try { + ThirdPartyPasswordlessRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { } + } + } + else if (recipeId === 'thirdparty') { + try { + ThirdPartyRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { } + + if (!isRecipeInitialised) { + try { + ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { } + } + + if (!isRecipeInitialised) { + try { + ThirdPartyPasswordlessRecipe.getInstanceOrThrowError() + isRecipeInitialised = true + } + catch (_) { } + } + } + + return isRecipeInitialised +} + +export async function validateApiKey(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }) { + let apiKeyHeaderValue: string | undefined = input.req.getHeaderValue('authorization') + + // We receieve the api key as `Bearer API_KEY`, this retrieves just the key + apiKeyHeaderValue = apiKeyHeaderValue?.split(' ')[1] + + if (apiKeyHeaderValue === undefined) + return false + + return apiKeyHeaderValue === input.config.apiKey +} + +export function getApiPathWithDashboardBase(path: string): string { + return DASHBOARD_API + path +} diff --git a/src/recipe/emailpassword/api/emailExists.ts b/src/recipe/emailpassword/api/emailExists.ts new file mode 100644 index 000000000..1ddb61243 --- /dev/null +++ b/src/recipe/emailpassword/api/emailExists.ts @@ -0,0 +1,43 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '../' + +export default async function emailExists(apiImplementation: APIInterface, options: APIOptions): Promise { + // Logic as per https://github.com/supertokens/supertokens-node/issues/47#issue-751571692 + + if (apiImplementation.emailExistsGET === undefined) + return false + + const email = options.req.getKeyValueFromQuery('email') + + if (email === undefined || typeof email !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the email as a GET param', + }) + } + + const result = await apiImplementation.emailExistsGET({ + email, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/emailpassword/api/generatePasswordResetToken.ts b/src/recipe/emailpassword/api/generatePasswordResetToken.ts new file mode 100644 index 000000000..48e953f0a --- /dev/null +++ b/src/recipe/emailpassword/api/generatePasswordResetToken.ts @@ -0,0 +1,46 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../' +import { validateFormFieldsOrThrowError } from './utils' + +export default async function generatePasswordResetToken( + apiImplementation: APIInterface, + options: APIOptions, +): Promise { + // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 + + if (apiImplementation.generatePasswordResetTokenPOST === undefined) + return false + + // step 1 + const formFields: { + id: string + value: string + }[] = await validateFormFieldsOrThrowError( + options.config.resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm, + (await options.req.getJSONBody()).formFields, + ) + + const result = await apiImplementation.generatePasswordResetTokenPOST({ + formFields, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/emailpassword/api/implementation.ts b/src/recipe/emailpassword/api/implementation.ts new file mode 100644 index 000000000..5a0a3d97a --- /dev/null +++ b/src/recipe/emailpassword/api/implementation.ts @@ -0,0 +1,200 @@ +import { APIInterface, APIOptions, User } from '../' +import { logDebugMessage } from '../../../logger' +import Session from '../../session' +import { SessionContainerInterface } from '../../session/types' +import { GeneralErrorResponse } from '../../../types' + +export default function getAPIImplementation(): APIInterface { + return { + async emailExistsGET({ + email, + options, + userContext, + }: { + email: string + options: APIOptions + userContext: any + }): Promise< + | { + status: 'OK' + exists: boolean + } + | GeneralErrorResponse + > { + const user = await options.recipeImplementation.getUserByEmail({ email, userContext }) + + return { + status: 'OK', + exists: user !== undefined, + } + }, + async generatePasswordResetTokenPOST({ + formFields, + options, + userContext, + }: { + formFields: { + id: string + value: string + }[] + options: APIOptions + userContext: any + }): Promise< + | { + status: 'OK' + } + | GeneralErrorResponse + > { + const email = formFields.filter(f => f.id === 'email')[0].value + + const user = await options.recipeImplementation.getUserByEmail({ email, userContext }) + if (user === undefined) { + return { + status: 'OK', + } + } + + const response = await options.recipeImplementation.createResetPasswordToken({ + userId: user.id, + userContext, + }) + if (response.status === 'UNKNOWN_USER_ID_ERROR') { + logDebugMessage(`Password reset email not sent, unknown user id: ${user.id}`) + return { + status: 'OK', + } + } + + const passwordResetLink + = `${options.appInfo.websiteDomain.getAsStringDangerous() + + options.appInfo.websiteBasePath.getAsStringDangerous() + }/reset-password?token=${ + response.token + }&rid=${ + options.recipeId}` + + logDebugMessage(`Sending password reset email to ${email}`) + await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: 'PASSWORD_RESET', + user, + passwordResetLink, + userContext, + }) + + return { + status: 'OK', + } + }, + async passwordResetPOST({ + formFields, + token, + options, + userContext, + }: { + formFields: { + id: string + value: string + }[] + token: string + options: APIOptions + userContext: any + }): Promise< + | { + status: 'OK' + /** + * The id of the user whose password was reset. + * Defined for Core versions 3.9 or later + */ + userId?: string + } + | { status: 'RESET_PASSWORD_INVALID_TOKEN_ERROR' } + | GeneralErrorResponse + > { + const newPassword = formFields.filter(f => f.id === 'password')[0].value + + const response = await options.recipeImplementation.resetPasswordUsingToken({ + token, + newPassword, + userContext, + }) + + return response + }, + async signInPOST({ + formFields, + options, + userContext, + }: { + formFields: { + id: string + value: string + }[] + options: APIOptions + userContext: any + }): Promise< + | { + status: 'OK' + session: SessionContainerInterface + user: User + } + | { + status: 'WRONG_CREDENTIALS_ERROR' + } + | GeneralErrorResponse + > { + const email = formFields.filter(f => f.id === 'email')[0].value + const password = formFields.filter(f => f.id === 'password')[0].value + + const response = await options.recipeImplementation.signIn({ email, password, userContext }) + if (response.status === 'WRONG_CREDENTIALS_ERROR') + return response + + const user = response.user + + const session = await Session.createNewSession(options.req, options.res, user.id, {}, {}, userContext) + return { + status: 'OK', + session, + user, + } + }, + async signUpPOST({ + formFields, + options, + userContext, + }: { + formFields: { + id: string + value: string + }[] + options: APIOptions + userContext: any + }): Promise< + | { + status: 'OK' + session: SessionContainerInterface + user: User + } + | { + status: 'EMAIL_ALREADY_EXISTS_ERROR' + } + | GeneralErrorResponse + > { + const email = formFields.filter(f => f.id === 'email')[0].value + const password = formFields.filter(f => f.id === 'password')[0].value + + const response = await options.recipeImplementation.signUp({ email, password, userContext }) + if (response.status === 'EMAIL_ALREADY_EXISTS_ERROR') + return response + + const user = response.user + + const session = await Session.createNewSession(options.req, options.res, user.id, {}, {}, userContext) + return { + status: 'OK', + session, + user, + } + }, + } +} diff --git a/src/recipe/emailpassword/api/passwordReset.ts b/src/recipe/emailpassword/api/passwordReset.ts new file mode 100644 index 000000000..461b413a1 --- /dev/null +++ b/src/recipe/emailpassword/api/passwordReset.ts @@ -0,0 +1,66 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '../' +import { validateFormFieldsOrThrowError } from './utils' + +export default async function passwordReset(apiImplementation: APIInterface, options: APIOptions): Promise { + // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 + + if (apiImplementation.passwordResetPOST === undefined) + return false + + // step 1 + const formFields: { + id: string + value: string + }[] = await validateFormFieldsOrThrowError( + options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, + (await options.req.getJSONBody()).formFields, + ) + + const token = (await options.req.getJSONBody()).token + if (token === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the password reset token', + }) + } + if (typeof token !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'The password reset token must be a string', + }) + } + + const result = await apiImplementation.passwordResetPOST({ + formFields, + token, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + send200Response( + options.res, + result.status === 'OK' + ? { + status: 'OK', + } + : result, + ) + return true +} diff --git a/src/recipe/emailpassword/api/signin.ts b/src/recipe/emailpassword/api/signin.ts new file mode 100644 index 000000000..c11816242 --- /dev/null +++ b/src/recipe/emailpassword/api/signin.ts @@ -0,0 +1,50 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../' +import { validateFormFieldsOrThrowError } from './utils' + +export default async function signInAPI(apiImplementation: APIInterface, options: APIOptions): Promise { + // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 + if (apiImplementation.signInPOST === undefined) + return false + + // step 1 + const formFields: { + id: string + value: string + }[] = await validateFormFieldsOrThrowError( + options.config.signInFeature.formFields, + (await options.req.getJSONBody()).formFields, + ) + + const result = await apiImplementation.signInPOST({ + formFields, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + if (result.status === 'OK') { + send200Response(options.res, { + status: 'OK', + user: result.user, + }) + } + else { + send200Response(options.res, result) + } + return true +} diff --git a/src/recipe/emailpassword/api/signup.ts b/src/recipe/emailpassword/api/signup.ts new file mode 100644 index 000000000..d45ed91e6 --- /dev/null +++ b/src/recipe/emailpassword/api/signup.ts @@ -0,0 +1,63 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../' +import STError from '../error' +import { validateFormFieldsOrThrowError } from './utils' + +export default async function signUpAPI(apiImplementation: APIInterface, options: APIOptions): Promise { + // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 + + if (apiImplementation.signUpPOST === undefined) + return false + + // step 1 + const formFields: { + id: string + value: string + }[] = await validateFormFieldsOrThrowError( + options.config.signUpFeature.formFields, + (await options.req.getJSONBody()).formFields, + ) + + const result = await apiImplementation.signUpPOST({ + formFields, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + if (result.status === 'OK') { + send200Response(options.res, { + status: 'OK', + user: result.user, + }) + } + else if (result.status === 'GENERAL_ERROR') { + send200Response(options.res, result) + } + else { + throw new STError({ + type: STError.FIELD_ERROR, + payload: [ + { + id: 'email', + error: 'This email already exists. Please sign in instead.', + }, + ], + message: 'Error in input formFields', + }) + } + return true +} diff --git a/src/recipe/emailpassword/api/utils.ts b/src/recipe/emailpassword/api/utils.ts new file mode 100644 index 000000000..ad9839312 --- /dev/null +++ b/src/recipe/emailpassword/api/utils.ts @@ -0,0 +1,123 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { NormalisedFormField } from '../types' +import STError from '../error' +import { FORM_FIELD_EMAIL_ID } from '../constants' + +export async function validateFormFieldsOrThrowError( + configFormFields: NormalisedFormField[], + formFieldsRaw: any, +): Promise< + { + id: string + value: string + }[] +> { + // first we check syntax ---------------------------- + if (formFieldsRaw === undefined) + throw newBadRequestError('Missing input param: formFields') + + if (!Array.isArray(formFieldsRaw)) + throw newBadRequestError('formFields must be an array') + + let formFields: { + id: string + value: string + }[] = [] + + for (let i = 0; i < formFieldsRaw.length; i++) { + const curr = formFieldsRaw[i] + if (typeof curr !== 'object' || curr === null) + throw newBadRequestError('All elements of formFields must be an object') + + if (typeof curr.id !== 'string' || curr.value === undefined) + throw newBadRequestError('All elements of formFields must contain an \'id\' and \'value\' field') + + formFields.push(curr) + } + + // we trim the email: https://github.com/supertokens/supertokens-core/issues/99 + formFields = formFields.map((field) => { + if (field.id === FORM_FIELD_EMAIL_ID) { + return { + ...field, + value: field.value.trim(), + } + } + return field + }) + + // then run validators through them----------------------- + await validateFormOrThrowError(formFields, configFormFields) + + return formFields +} + +function newBadRequestError(message: string) { + return new STError({ + type: STError.BAD_INPUT_ERROR, + message, + }) +} + +// We check that the number of fields in input and config form field is the same. +// We check that each item in the config form field is also present in the input form field +async function validateFormOrThrowError( + inputs: { + id: string + value: string + }[], + configFormFields: NormalisedFormField[], +) { + const validationErrors: { id: string; error: string }[] = [] + + if (configFormFields.length !== inputs.length) + throw newBadRequestError('Are you sending too many / too few formFields?') + + // Loop through all form fields. + for (let i = 0; i < configFormFields.length; i++) { + const field = configFormFields[i] + + // Find corresponding input value. + const input = inputs.find(i => i.id === field.id) + + // Absent or not optional empty field + if (input === undefined || (input.value === '' && !field.optional)) { + validationErrors.push({ + error: 'Field is not optional', + id: field.id, + }) + } + else { + // Otherwise, use validate function. + const error = await field.validate(input.value) + // If error, add it. + if (error !== undefined) { + validationErrors.push({ + error, + id: field.id, + }) + } + } + } + + if (validationErrors.length !== 0) { + throw new STError({ + type: STError.FIELD_ERROR, + payload: validationErrors, + message: 'Error in input formFields', + }) + } +} diff --git a/src/recipe/emailpassword/constants.ts b/src/recipe/emailpassword/constants.ts new file mode 100644 index 000000000..f71f5ff99 --- /dev/null +++ b/src/recipe/emailpassword/constants.ts @@ -0,0 +1,28 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const FORM_FIELD_PASSWORD_ID = 'password' + +export const FORM_FIELD_EMAIL_ID = 'email' + +export const SIGN_UP_API = '/signup' + +export const SIGN_IN_API = '/signin' + +export const GENERATE_PASSWORD_RESET_TOKEN_API = '/user/password/reset/token' + +export const PASSWORD_RESET_API = '/user/password/reset' + +export const SIGNUP_EMAIL_EXISTS_API = '/signup/email/exists' diff --git a/src/recipe/emailpassword/emaildelivery/index.ts b/src/recipe/emailpassword/emaildelivery/index.ts new file mode 100644 index 000000000..ef147a362 --- /dev/null +++ b/src/recipe/emailpassword/emaildelivery/index.ts @@ -0,0 +1 @@ +export * from './services' diff --git a/src/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts b/src/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts new file mode 100644 index 000000000..2b67dac36 --- /dev/null +++ b/src/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -0,0 +1,86 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { RecipeInterface, TypeEmailPasswordEmailDeliveryInput, User } from '../../../types' +import { createAndSendCustomEmail as defaultCreateAndSendCustomEmail } from '../../../passwordResetFunctions' +import { NormalisedAppinfo } from '../../../../../types' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' + +export default class BackwardCompatibilityService +implements EmailDeliveryInterface { + private recipeInterfaceImpl: RecipeInterface + private isInServerlessEnv: boolean + private appInfo: NormalisedAppinfo + private resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise + } + + constructor( + recipeInterfaceImpl: RecipeInterface, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + resetPasswordUsingTokenFeature?: { + createAndSendCustomEmail?: ( + user: User, + passwordResetURLWithToken: string, + userContext: any + ) => Promise + }, + ) { + this.recipeInterfaceImpl = recipeInterfaceImpl + this.isInServerlessEnv = isInServerlessEnv + this.appInfo = appInfo + { + const inputCreateAndSendCustomEmail = resetPasswordUsingTokenFeature?.createAndSendCustomEmail + this.resetPasswordUsingTokenFeature + = inputCreateAndSendCustomEmail !== undefined + ? { + createAndSendCustomEmail: inputCreateAndSendCustomEmail, + } + : { + createAndSendCustomEmail: defaultCreateAndSendCustomEmail(this.appInfo), + } + } + } + + sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }) => { + const user = await this.recipeInterfaceImpl.getUserById({ + userId: input.user.id, + userContext: input.userContext, + }) + if (user === undefined) + throw new Error('this should never come here') + + // we add this here cause the user may have overridden the sendEmail function + // to change the input email and if we don't do this, the input email + // will get reset by the getUserById call above. + user.email = input.user.email + try { + if (!this.isInServerlessEnv) { + this.resetPasswordUsingTokenFeature + .createAndSendCustomEmail(user, input.passwordResetLink, input.userContext) + .catch((_) => {}) + } + else { + // see https://github.com/supertokens/supertokens-node/pull/135 + await this.resetPasswordUsingTokenFeature.createAndSendCustomEmail( + user, + input.passwordResetLink, + input.userContext, + ) + } + } + catch (_) {} + } +} diff --git a/src/recipe/emailpassword/emaildelivery/services/index.ts b/src/recipe/emailpassword/emaildelivery/services/index.ts new file mode 100644 index 000000000..6e257c914 --- /dev/null +++ b/src/recipe/emailpassword/emaildelivery/services/index.ts @@ -0,0 +1,17 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import SMTP from './smtp' +export const SMTPService = SMTP diff --git a/src/recipe/emailpassword/emaildelivery/services/smtp/index.ts b/src/recipe/emailpassword/emaildelivery/services/smtp/index.ts new file mode 100644 index 000000000..bb2141c01 --- /dev/null +++ b/src/recipe/emailpassword/emaildelivery/services/smtp/index.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { createTransport } from 'nodemailer' +import OverrideableBuilder from 'overrideableBuilder' +import { ServiceInterface, TypeInput } from '../../../../../ingredients/emaildelivery/services/smtp' +import { TypeEmailPasswordEmailDeliveryInput } from '../../../types' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' +import { getServiceImplementation } from './serviceImplementation' + +export default class SMTPService implements EmailDeliveryInterface { + serviceImpl: ServiceInterface + + constructor(config: TypeInput) { + const transporter = createTransport({ + host: config.smtpSettings.host, + port: config.smtpSettings.port, + auth: { + user: config.smtpSettings.authUsername || config.smtpSettings.from.email, + pass: config.smtpSettings.password, + }, + secure: config.smtpSettings.secure, + }) + let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)) + if (config.override !== undefined) + builder = builder.override(config.override) + + this.serviceImpl = builder.build() + } + + sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }) => { + const content = await this.serviceImpl.getContent(input) + await this.serviceImpl.sendRawEmail({ + ...content, + userContext: input.userContext, + }) + } +} diff --git a/src/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.ts b/src/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.ts new file mode 100644 index 000000000..1d53720b8 --- /dev/null +++ b/src/recipe/emailpassword/emaildelivery/services/smtp/passwordReset.ts @@ -0,0 +1,940 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeEmailPasswordPasswordResetEmailDeliveryInput } from '../../../types' +import { GetContentResult } from '../../../../../ingredients/emaildelivery/services/smtp' +import Supertokens from '../../../../../supertokens' +export default function getPasswordResetEmailContent( + input: TypeEmailPasswordPasswordResetEmailDeliveryInput, +): GetContentResult { + const supertokens = Supertokens.getInstanceOrThrowError() + const appName = supertokens.appInfo.appName + const body = getPasswordResetEmailHTML(appName, input.user.email, input.passwordResetLink) + return { + body, + toEmail: input.user.email, + subject: 'Password reset instructions', + isHtml: true, + } +} + +export function getPasswordResetEmailHTML(appName: string, email: string, resetLink: string) { + return ` + + + + + + + + *|MC:SUBJECT|* + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + + + + +
+ + +
+
+ +

+ A password reset request for your account on + ${appName} has been received. +

+ + +
+
+

+ Alternatively, you can directly paste this link + in your browser
+ ${resetLink} +

+
+
+ + + + +
+ + + + + + +
+

+ This email is meant for ${email} +

+
+
+ +
+ + + + + +
+ +
+ +
+
+ + + + ` +} diff --git a/src/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts b/src/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts new file mode 100644 index 000000000..2f7ec401f --- /dev/null +++ b/src/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts @@ -0,0 +1,57 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { Transporter } from 'nodemailer' +import { TypeEmailPasswordEmailDeliveryInput } from '../../../../types' +import { + GetContentResult, + ServiceInterface, + TypeInputSendRawEmail, +} from '../../../../../../ingredients/emaildelivery/services/smtp' +import getPasswordResetEmailContent from '../passwordReset' + +export function getServiceImplementation( + transporter: Transporter, + from: { + name: string + email: string + }, +): ServiceInterface { + return { + async sendRawEmail(input: TypeInputSendRawEmail) { + if (input.isHtml) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, + }) + } + else { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + text: input.body, + }) + } + }, + async getContent( + input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }, + ): Promise { + return getPasswordResetEmailContent(input) + }, + } +} diff --git a/src/recipe/emailpassword/error.ts b/src/recipe/emailpassword/error.ts new file mode 100644 index 000000000..540f025b3 --- /dev/null +++ b/src/recipe/emailpassword/error.ts @@ -0,0 +1,41 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../../error' + +export default class SessionError extends STError { + static FIELD_ERROR = 'FIELD_ERROR' as const + + constructor( + options: + | { + type: 'FIELD_ERROR' + payload: { + id: string + error: string + }[] + message: string + } + | { + type: 'BAD_INPUT_ERROR' + message: string + }, + ) { + super({ + ...options, + }) + this.fromRecipe = 'emailpassword' + } +} diff --git a/src/recipe/emailpassword/index.ts b/src/recipe/emailpassword/index.ts new file mode 100644 index 000000000..89dfce221 --- /dev/null +++ b/src/recipe/emailpassword/index.ts @@ -0,0 +1,106 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import Recipe from './recipe' +import SuperTokensError from './error' +import { APIInterface, APIOptions, RecipeInterface, TypeEmailPasswordEmailDeliveryInput, User } from './types' + +export default class Wrapper { + static init = Recipe.init + + static Error = SuperTokensError + + static signUp(email: string, password: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signUp({ + email, + password, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static signIn(email: string, password: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signIn({ + email, + password, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static getUserById(userId: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ + userId, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static getUserByEmail(email: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ + email, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static createResetPasswordToken(userId: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ + userId, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static resetPasswordUsingToken(token: string, newPassword: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + token, + newPassword, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static updateEmailOrPassword(input: { userId: string; email?: string; password?: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ + userContext: {}, + ...input, + }) + } + + static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any }) { + const recipeInstance = Recipe.getInstanceOrThrowError() + return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail({ + userContext: {}, + ...input, + }) + } +} + +export const init = Wrapper.init + +export const Error = Wrapper.Error + +export const signUp = Wrapper.signUp + +export const signIn = Wrapper.signIn + +export const getUserById = Wrapper.getUserById + +export const getUserByEmail = Wrapper.getUserByEmail + +export const createResetPasswordToken = Wrapper.createResetPasswordToken + +export const resetPasswordUsingToken = Wrapper.resetPasswordUsingToken + +export const updateEmailOrPassword = Wrapper.updateEmailOrPassword + +export type { RecipeInterface, User, APIOptions, APIInterface } + +export const sendEmail = Wrapper.sendEmail diff --git a/src/recipe/emailpassword/passwordResetFunctions.ts b/src/recipe/emailpassword/passwordResetFunctions.ts new file mode 100644 index 000000000..e26828411 --- /dev/null +++ b/src/recipe/emailpassword/passwordResetFunctions.ts @@ -0,0 +1,71 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import axios, { AxiosError } from 'axios' +import { NormalisedAppinfo } from '../../types' +import { logDebugMessage } from '../../logger' +import { User } from './types' + +export function createAndSendCustomEmail(appInfo: NormalisedAppinfo) { + return async (user: User, passwordResetURLWithToken: string) => { + // related issue: https://github.com/supertokens/supertokens-node/issues/38 + if (process.env.TEST_MODE === 'testing') + return + + try { + await axios({ + method: 'POST', + url: 'https://api.supertokens.io/0/st/auth/password/reset', + data: { + email: user.email, + appName: appInfo.appName, + passwordResetURL: passwordResetURLWithToken, + }, + headers: { + 'api-version': 0, + }, + }) + logDebugMessage(`Password reset email sent to ${user.email}`) + } + catch (error) { + logDebugMessage('Error sending password reset email') + if (axios.isAxiosError(error)) { + const err = error as AxiosError + if (err.response) { + logDebugMessage(`Error status: ${err.response.status}`) + logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`) + } + else { + logDebugMessage(`Error: ${err.message}`) + } + } + else { + logDebugMessage(`Error: ${JSON.stringify(error)}`) + } + logDebugMessage('Logging the input below:') + logDebugMessage( + JSON.stringify( + { + email: user.email, + appName: appInfo.appName, + passwordResetURL: passwordResetURLWithToken, + }, + null, + 2, + ), + ) + } + } +} diff --git a/src/recipe/emailpassword/recipe.ts b/src/recipe/emailpassword/recipe.ts new file mode 100644 index 000000000..eebb00428 --- /dev/null +++ b/src/recipe/emailpassword/recipe.ts @@ -0,0 +1,233 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import NormalisedURLPath from '../../normalisedURLPath' +import { send200Response } from '../../utils' +import EmailVerificationRecipe from '../emailverification/recipe' +import { Querier } from '../../querier' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import { PostSuperTokensInitCallbacks } from '../../postSuperTokensInitCallbacks' +import { GetEmailForUserIdFunc } from '../emailverification/types' +import { APIInterface, RecipeInterface, TypeEmailPasswordEmailDeliveryInput, TypeInput, TypeNormalisedInput } from './types' +import STError from './error' +import { validateAndNormaliseUserInput } from './utils' +import { + GENERATE_PASSWORD_RESET_TOKEN_API, + PASSWORD_RESET_API, + SIGNUP_EMAIL_EXISTS_API, + SIGN_IN_API, + SIGN_UP_API, +} from './constants' +import signUpAPI from './api/signup' +import signInAPI from './api/signin' +import generatePasswordResetTokenAPI from './api/generatePasswordResetToken' +import passwordResetAPI from './api/passwordReset' +import emailExistsAPI from './api/emailExists' +import RecipeImplementation from './recipeImplementation' +import APIImplementation from './api/implementation' + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined + static RECIPE_ID = 'emailpassword' + + config: TypeNormalisedInput + + recipeInterfaceImpl: RecipeInterface + + apiImpl: APIInterface + + isInServerlessEnv: boolean + + emailDelivery: EmailDeliveryIngredient + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput | undefined, + ingredients: { + emailDelivery: EmailDeliveryIngredient | undefined + }, + ) { + super(recipeId, appInfo) + this.isInServerlessEnv = isInServerlessEnv + this.config = validateAndNormaliseUserInput(this, appInfo, config) + { + const builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + + /** + * emailDelivery will always needs to be declared after isInServerlessEnv + * and recipeInterfaceImpl values are set + */ + this.emailDelivery + = ingredients.emailDelivery === undefined + ? new EmailDeliveryIngredient( + this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv), + ) + : ingredients.emailDelivery + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const emailVerificationRecipe = EmailVerificationRecipe.getInstance() + if (emailVerificationRecipe !== undefined) + emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)) + }) + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { + emailDelivery: undefined, + }) + return Recipe.instance + } + else { + throw new Error('Emailpassword recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + // abstract instance functions below............... + + getAPIsHandled = (): APIHandled[] => { + return [ + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(SIGN_UP_API), + id: SIGN_UP_API, + disabled: this.apiImpl.signUpPOST === undefined, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(SIGN_IN_API), + id: SIGN_IN_API, + disabled: this.apiImpl.signInPOST === undefined, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(GENERATE_PASSWORD_RESET_TOKEN_API), + id: GENERATE_PASSWORD_RESET_TOKEN_API, + disabled: this.apiImpl.generatePasswordResetTokenPOST === undefined, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(PASSWORD_RESET_API), + id: PASSWORD_RESET_API, + disabled: this.apiImpl.passwordResetPOST === undefined, + }, + { + method: 'get', + pathWithoutApiBasePath: new NormalisedURLPath(SIGNUP_EMAIL_EXISTS_API), + id: SIGNUP_EMAIL_EXISTS_API, + disabled: this.apiImpl.emailExistsGET === undefined, + }, + ] + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + ): Promise => { + const options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + emailDelivery: this.emailDelivery, + appInfo: this.getAppInfo(), + } + if (id === SIGN_UP_API) + return await signUpAPI(this.apiImpl, options) + else if (id === SIGN_IN_API) + return await signInAPI(this.apiImpl, options) + else if (id === GENERATE_PASSWORD_RESET_TOKEN_API) + return await generatePasswordResetTokenAPI(this.apiImpl, options) + else if (id === PASSWORD_RESET_API) + return await passwordResetAPI(this.apiImpl, options) + else if (id === SIGNUP_EMAIL_EXISTS_API) + return await emailExistsAPI(this.apiImpl, options) + + return false + } + + handleError = async (err: STError, _request: BaseRequest, response: BaseResponse): Promise => { + if (err.fromRecipe === Recipe.RECIPE_ID) { + if (err.type === STError.FIELD_ERROR) { + return send200Response(response, { + status: 'FIELD_ERROR', + formFields: err.payload, + }) + } + else { + throw err + } + } + else { + throw err + } + } + + getAllCORSHeaders = (): string[] => { + return [] + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } + + // extra instance functions below............... + getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { + const userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }) + if (userInfo !== undefined) { + return { + status: 'OK', + email: userInfo.email, + } + } + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } +} diff --git a/src/recipe/emailpassword/recipeImplementation.ts b/src/recipe/emailpassword/recipeImplementation.ts new file mode 100644 index 000000000..ae37de48f --- /dev/null +++ b/src/recipe/emailpassword/recipeImplementation.ts @@ -0,0 +1,150 @@ +import { Querier } from '../../querier' +import NormalisedURLPath from '../../normalisedURLPath' +import { RecipeInterface, User } from './types' + +export default function getRecipeInterface(querier: Querier): RecipeInterface { + return { + async signUp({ + email, + password, + }: { + email: string + password: string + }): Promise<{ status: 'OK'; user: User } | { status: 'EMAIL_ALREADY_EXISTS_ERROR' }> { + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/signup'), { + email, + password, + }) + if (response.status === 'OK') { + return response + } + else { + return { + status: 'EMAIL_ALREADY_EXISTS_ERROR', + } + } + }, + + async signIn({ + email, + password, + }: { + email: string + password: string + }): Promise<{ status: 'OK'; user: User } | { status: 'WRONG_CREDENTIALS_ERROR' }> { + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/signin'), { + email, + password, + }) + if (response.status === 'OK') { + return response + } + else { + return { + status: 'WRONG_CREDENTIALS_ERROR', + } + } + }, + + async getUserById({ userId }: { userId: string }): Promise { + const response = await querier.sendGetRequest(new NormalisedURLPath('/recipe/user'), { + userId, + }) + if (response.status === 'OK') { + return { + ...response.user, + } + } + else { + return undefined + } + }, + + async getUserByEmail({ email }: { email: string }): Promise { + const response = await querier.sendGetRequest(new NormalisedURLPath('/recipe/user'), { + email, + }) + if (response.status === 'OK') { + return { + ...response.user, + } + } + else { + return undefined + } + }, + + async createResetPasswordToken({ + userId, + }: { + userId: string + }): Promise<{ status: 'OK'; token: string } | { status: 'UNKNOWN_USER_ID_ERROR' }> { + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/user/password/reset/token'), { + userId, + }) + if (response.status === 'OK') { + return { + status: 'OK', + token: response.token, + } + } + else { + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } + }, + + async resetPasswordUsingToken({ + token, + newPassword, + }: { + token: string + newPassword: string + }): Promise< + | { + status: 'OK' + /** + * The id of the user whose password was reset. + * Defined for Core versions 3.9 or later + */ + userId?: string + } + | { status: 'RESET_PASSWORD_INVALID_TOKEN_ERROR' } + > { + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/user/password/reset'), { + method: 'token', + token, + newPassword, + }) + return response + }, + + async updateEmailOrPassword(input: { + userId: string + email?: string + password?: string + }): Promise<{ status: 'OK' | 'UNKNOWN_USER_ID_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' }> { + const response = await querier.sendPutRequest(new NormalisedURLPath('/recipe/user'), { + userId: input.userId, + email: input.email, + password: input.password, + }) + if (response.status === 'OK') { + return { + status: 'OK', + } + } + else if (response.status === 'EMAIL_ALREADY_EXISTS_ERROR') { + return { + status: 'EMAIL_ALREADY_EXISTS_ERROR', + } + } + else { + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } + }, + } +} diff --git a/src/recipe/emailpassword/types.ts b/src/recipe/emailpassword/types.ts new file mode 100644 index 000000000..4c365822c --- /dev/null +++ b/src/recipe/emailpassword/types.ts @@ -0,0 +1,262 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { SessionContainerInterface } from '../session/types' +import { + TypeInput as EmailDeliveryTypeInput, + TypeInputWithService as EmailDeliveryTypeInputWithService, +} from '../../ingredients/emaildelivery/types' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import { GeneralErrorResponse, NormalisedAppinfo } from '../../types' + +export interface TypeNormalisedInput { + signUpFeature: TypeNormalisedInputSignUp + signInFeature: TypeNormalisedInputSignIn + getEmailDeliveryConfig: ( + recipeImpl: RecipeInterface, + isInServerlessEnv: boolean + ) => EmailDeliveryTypeInputWithService + resetPasswordUsingTokenFeature: TypeNormalisedInputResetPasswordUsingTokenFeature + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeInputFormField { + id: string + validate?: (value: any) => Promise + optional?: boolean +} + +export interface TypeFormField { id: string; value: any } + +export interface TypeInputSignUp { + formFields?: TypeInputFormField[] +} + +export interface NormalisedFormField { + id: string + validate: (value: any) => Promise + optional: boolean +} + +export interface TypeNormalisedInputSignUp { + formFields: NormalisedFormField[] +} + +export interface TypeNormalisedInputSignIn { + formFields: NormalisedFormField[] +} + +export interface TypeInputResetPasswordUsingTokenFeature { + /** + * @deprecated Please use emailDelivery config instead + */ + createAndSendCustomEmail?: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise +} + +export interface TypeNormalisedInputResetPasswordUsingTokenFeature { + formFieldsForGenerateTokenForm: NormalisedFormField[] + formFieldsForPasswordResetForm: NormalisedFormField[] +} + +export interface User { + id: string + email: string + timeJoined: number +} + +export interface TypeInput { + signUpFeature?: TypeInputSignUp + emailDelivery?: EmailDeliveryTypeInput + resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface RecipeInterface { + signUp(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'EMAIL_ALREADY_EXISTS_ERROR' }> + + signIn(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'WRONG_CREDENTIALS_ERROR' }> + + getUserById(input: { userId: string; userContext: any }): Promise + + getUserByEmail(input: { email: string; userContext: any }): Promise + + createResetPasswordToken(input: { + userId: string + userContext: any + }): Promise<{ status: 'OK'; token: string } | { status: 'UNKNOWN_USER_ID_ERROR' }> + + resetPasswordUsingToken(input: { + token: string + newPassword: string + userContext: any + }): Promise< + | { + status: 'OK' + /** + * The id of the user whose password was reset. + * Defined for Core versions 3.9 or later + */ + userId?: string + } + | { status: 'RESET_PASSWORD_INVALID_TOKEN_ERROR' } + > + + updateEmailOrPassword(input: { + userId: string + email?: string + password?: string + userContext: any + }): Promise<{ status: 'OK' | 'UNKNOWN_USER_ID_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' }> +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + appInfo: NormalisedAppinfo + config: TypeNormalisedInput + recipeId: string + isInServerlessEnv: boolean + req: BaseRequest + res: BaseResponse + emailDelivery: EmailDeliveryIngredient +} + +export interface APIInterface { + emailExistsGET: + | undefined + | ((input: { + email: string + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + exists: boolean + } + | GeneralErrorResponse + >) + + generatePasswordResetTokenPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + } + | GeneralErrorResponse + >) + + passwordResetPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + token: string + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + userId?: string + } + | { + status: 'RESET_PASSWORD_INVALID_TOKEN_ERROR' + } + | GeneralErrorResponse + >) + + signInPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + user: User + session: SessionContainerInterface + } + | { + status: 'WRONG_CREDENTIALS_ERROR' + } + | GeneralErrorResponse + >) + + signUpPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + user: User + session: SessionContainerInterface + } + | { + status: 'EMAIL_ALREADY_EXISTS_ERROR' + } + | GeneralErrorResponse + >) +} + +export interface TypeEmailPasswordPasswordResetEmailDeliveryInput { + type: 'PASSWORD_RESET' + user: { + id: string + email: string + } + passwordResetLink: string +} + +export type TypeEmailPasswordEmailDeliveryInput = TypeEmailPasswordPasswordResetEmailDeliveryInput diff --git a/src/recipe/emailpassword/utils.ts b/src/recipe/emailpassword/utils.ts new file mode 100644 index 000000000..daa8a04f7 --- /dev/null +++ b/src/recipe/emailpassword/utils.ts @@ -0,0 +1,252 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import Recipe from './recipe' +import { + APIInterface, + NormalisedFormField, + RecipeInterface, + TypeInput, + TypeInputFormField, + TypeInputSignUp, + TypeNormalisedInput, + TypeNormalisedInputResetPasswordUsingTokenFeature, + TypeNormalisedInputSignIn, TypeNormalisedInputSignUp, +} from './types' +import { FORM_FIELD_EMAIL_ID, FORM_FIELD_PASSWORD_ID } from './constants' +import BackwardCompatibilityService from './emaildelivery/services/backwardCompatibility' + +export function validateAndNormaliseUserInput( + recipeInstance: Recipe, + appInfo: NormalisedAppinfo, + config?: TypeInput, +): TypeNormalisedInput { + const signUpFeature = validateAndNormaliseSignupConfig( + recipeInstance, + appInfo, + config === undefined ? undefined : config.signUpFeature, + ) + + const signInFeature = validateAndNormaliseSignInConfig(recipeInstance, appInfo, signUpFeature) + + const resetPasswordUsingTokenFeature = validateAndNormaliseResetPasswordUsingTokenConfig(signUpFeature) + + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + function getEmailDeliveryConfig(recipeImpl: RecipeInterface, isInServerlessEnv: boolean) { + let emailService = config?.emailDelivery?.service + /** + * following code is for backward compatibility. + * if user has not passed emailDelivery config, we + * use the createAndSendCustomEmail config. If the user + * has not passed even that config, we use the default + * createAndSendCustomEmail implementation which calls our supertokens API + */ + if (emailService === undefined) { + emailService = new BackwardCompatibilityService( + recipeImpl, + appInfo, + isInServerlessEnv, + config?.resetPasswordUsingTokenFeature, + ) + } + return { + ...config?.emailDelivery, + /** + * if we do + * let emailDelivery = { + * service: emailService, + * ...config.emailDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + service: emailService, + } + } + return { + signUpFeature, + signInFeature, + resetPasswordUsingTokenFeature, + override, + getEmailDeliveryConfig, + } +} + +function validateAndNormaliseResetPasswordUsingTokenConfig( + signUpConfig: TypeNormalisedInputSignUp, +): TypeNormalisedInputResetPasswordUsingTokenFeature { + const formFieldsForPasswordResetForm: NormalisedFormField[] = signUpConfig.formFields + .filter(filter => filter.id === FORM_FIELD_PASSWORD_ID) + .map((field) => { + return { + id: field.id, + validate: field.validate, + optional: false, + } + }) + + const formFieldsForGenerateTokenForm: NormalisedFormField[] = signUpConfig.formFields + .filter(filter => filter.id === FORM_FIELD_EMAIL_ID) + .map((field) => { + return { + id: field.id, + validate: field.validate, + optional: false, + } + }) + + return { + formFieldsForPasswordResetForm, + formFieldsForGenerateTokenForm, + } +} + +function normaliseSignInFormFields(formFields: NormalisedFormField[]) { + return formFields + .filter(filter => filter.id === FORM_FIELD_EMAIL_ID || filter.id === FORM_FIELD_PASSWORD_ID) + .map((field) => { + return { + id: field.id, + // see issue: https://github.com/supertokens/supertokens-node/issues/36 + validate: field.id === FORM_FIELD_EMAIL_ID ? field.validate : defaultValidator, + optional: false, + } + }) +} + +function validateAndNormaliseSignInConfig( + _: Recipe, + __: NormalisedAppinfo, + signUpConfig: TypeNormalisedInputSignUp, +): TypeNormalisedInputSignIn { + const formFields: NormalisedFormField[] = normaliseSignInFormFields(signUpConfig.formFields) + + return { + formFields, + } +} + +export function normaliseSignUpFormFields(formFields?: TypeInputFormField[]): NormalisedFormField[] { + const normalisedFormFields: NormalisedFormField[] = [] + if (formFields !== undefined) { + formFields.forEach((field) => { + if (field.id === FORM_FIELD_PASSWORD_ID) { + normalisedFormFields.push({ + id: field.id, + validate: field.validate === undefined ? defaultPasswordValidator : field.validate, + optional: false, + }) + } + else if (field.id === FORM_FIELD_EMAIL_ID) { + normalisedFormFields.push({ + id: field.id, + validate: field.validate === undefined ? defaultEmailValidator : field.validate, + optional: false, + }) + } + else { + normalisedFormFields.push({ + id: field.id, + validate: field.validate === undefined ? defaultValidator : field.validate, + optional: field.optional === undefined ? false : field.optional, + }) + } + }) + } + if (normalisedFormFields.filter(field => field.id === FORM_FIELD_PASSWORD_ID).length === 0) { + // no password field give by user + normalisedFormFields.push({ + id: FORM_FIELD_PASSWORD_ID, + validate: defaultPasswordValidator, + optional: false, + }) + } + if (normalisedFormFields.filter(field => field.id === FORM_FIELD_EMAIL_ID).length === 0) { + // no email field give by user + normalisedFormFields.push({ + id: FORM_FIELD_EMAIL_ID, + validate: defaultEmailValidator, + optional: false, + }) + } + return normalisedFormFields +} + +function validateAndNormaliseSignupConfig( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInputSignUp, +): TypeNormalisedInputSignUp { + const formFields: NormalisedFormField[] = normaliseSignUpFormFields( + config === undefined ? undefined : config.formFields, + ) + + return { + formFields, + } +} + +async function defaultValidator(_: any): Promise { + return undefined +} + +export async function defaultPasswordValidator(value: any) { + // length >= 8 && < 100 + // must have a number and a character + // as per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 + + if (typeof value !== 'string') + return 'Development bug: Please make sure the password field yields a string' + + if (value.length < 8) + return 'Password must contain at least 8 characters, including a number' + + if (value.length >= 100) + return 'Password\'s length must be lesser than 100 characters' + + if (value.match(/^.*[A-Za-z]+.*$/) === null) + return 'Password must contain at least one alphabet' + + if (value.match(/^.*[0-9]+.*$/) === null) + return 'Password must contain at least one number' + + return undefined +} + +export async function defaultEmailValidator(value: any) { + // We check if the email syntax is correct + // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 + // Regex from https://stackoverflow.com/a/46181/3867175 + + if (typeof value !== 'string') + return 'Development bug: Please make sure the email field yields a string' + + if ( + value.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ) === null + ) + return 'Email is invalid' + + return undefined +} diff --git a/src/recipe/emailverification/api/emailVerify.ts b/src/recipe/emailverification/api/emailVerify.ts new file mode 100644 index 000000000..aaf114110 --- /dev/null +++ b/src/recipe/emailverification/api/emailVerify.ts @@ -0,0 +1,82 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, normaliseHttpMethod, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '../' +import Session from '../../session' + +export default async function emailVerify(apiImplementation: APIInterface, options: APIOptions): Promise { + let result + + const userContext = makeDefaultUserContextFromAPI(options.req) + + if (normaliseHttpMethod(options.req.getMethod()) === 'post') { + // Logic according to Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 + + if (apiImplementation.verifyEmailPOST === undefined) + return false + + const token = (await options.req.getJSONBody()).token + if (token === undefined || token === null) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the email verification token', + }) + } + if (typeof token !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'The email verification token must be a string', + }) + } + + const session = await Session.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [], sessionRequired: false }, + userContext, + ) + + const response = await apiImplementation.verifyEmailPOST({ + token, + options, + session, + userContext, + }) + if (response.status === 'OK') + result = { status: 'OK' } + else + result = response + } + else { + if (apiImplementation.isEmailVerifiedGET === undefined) + return false + + const session = await Session.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext, + ) + result = await apiImplementation.isEmailVerifiedGET({ + options, + session: session!, + userContext, + }) + } + send200Response(options.res, result) + return true +} diff --git a/src/recipe/emailverification/api/generateEmailVerifyToken.ts b/src/recipe/emailverification/api/generateEmailVerifyToken.ts new file mode 100644 index 000000000..8d4e42160 --- /dev/null +++ b/src/recipe/emailverification/api/generateEmailVerifyToken.ts @@ -0,0 +1,45 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../' +import Session from '../../session' + +export default async function generateEmailVerifyToken( + apiImplementation: APIInterface, + options: APIOptions, +): Promise { + // Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 + + if (apiImplementation.generateEmailVerifyTokenPOST === undefined) + return false + + const userContext = makeDefaultUserContextFromAPI(options.req) + const session = await Session.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext, + ) + + const result = await apiImplementation.generateEmailVerifyTokenPOST({ + options, + session: session!, + userContext, + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/emailverification/api/implementation.ts b/src/recipe/emailverification/api/implementation.ts new file mode 100644 index 000000000..48a802fe2 --- /dev/null +++ b/src/recipe/emailverification/api/implementation.ts @@ -0,0 +1,151 @@ +import { APIInterface, User } from '../' +import { logDebugMessage } from '../../../logger' +import EmailVerificationRecipe from '../recipe' +import { GeneralErrorResponse } from '../../../types' +import { EmailVerificationClaim } from '../emailVerificationClaim' +import SessionError from '../../session/error' +import { getEmailVerifyLink } from '../utils' + +export default function getAPIInterface(): APIInterface { + return { + async verifyEmailPOST({ + token, + options, + session, + userContext, + }): Promise< + { status: 'OK'; user: User } | { status: 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR' } | GeneralErrorResponse + > { + const res = await options.recipeImplementation.verifyEmailUsingToken({ token, userContext }) + + if (res.status === 'OK' && session !== undefined) { + try { + await session.fetchAndSetClaim(EmailVerificationClaim, userContext) + } + catch (err) { + // This should never happen, since we've just set the status above. + if ((err as Error).message === 'UNKNOWN_USER_ID') { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: 'Unknown User ID provided', + }) + } + throw err + } + } + return res + }, + + async isEmailVerifiedGET({ + userContext, + session, + }): Promise< + | { + status: 'OK' + isVerified: boolean + } + | GeneralErrorResponse + > { + if (session === undefined) + throw new Error('Session is undefined. Should not come here.') + + try { + await session.fetchAndSetClaim(EmailVerificationClaim, userContext) + } + catch (err) { + if ((err as Error).message === 'UNKNOWN_USER_ID') { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: 'Unknown User ID provided', + }) + } + throw err + } + const isVerified = await session.getClaimValue(EmailVerificationClaim, userContext) + + if (isVerified === undefined) + throw new Error('Should never come here: EmailVerificationClaim failed to set value') + + return { + status: 'OK', + isVerified, + } + }, + + async generateEmailVerifyTokenPOST({ + options, + userContext, + session, + }): Promise<{ status: 'OK' | 'EMAIL_ALREADY_VERIFIED_ERROR' } | GeneralErrorResponse> { + if (session === undefined) + throw new Error('Session is undefined. Should not come here.') + + const userId = session.getUserId() + + const emailInfo = await EmailVerificationRecipe.getInstanceOrThrowError().getEmailForUserId( + userId, + userContext, + ) + + if (emailInfo.status === 'EMAIL_DOES_NOT_EXIST_ERROR') { + logDebugMessage( + `Email verification email not sent to user ${userId} because it doesn't have an email address.`, + ) + return { + status: 'EMAIL_ALREADY_VERIFIED_ERROR', + } + } + else if (emailInfo.status === 'OK') { + const response = await options.recipeImplementation.createEmailVerificationToken({ + userId, + email: emailInfo.email, + userContext, + }) + + if (response.status === 'EMAIL_ALREADY_VERIFIED_ERROR') { + if ((await session.getClaimValue(EmailVerificationClaim)) !== true) { + // this can happen if the email was verified in another browser + // and this session is still outdated - and the user has not + // called the get email verification API yet. + await session.fetchAndSetClaim(EmailVerificationClaim, userContext) + } + logDebugMessage( + `Email verification email not sent to ${emailInfo.email} because it is already verified.`, + ) + return response + } + + if ((await session.getClaimValue(EmailVerificationClaim)) !== false) { + // this can happen if the email was unverified in another browser + // and this session is still outdated - and the user has not + // called the get email verification API yet. + await session.fetchAndSetClaim(EmailVerificationClaim, userContext) + } + + const emailVerifyLink = getEmailVerifyLink({ + appInfo: options.appInfo, + token: response.token, + recipeId: options.recipeId, + }) + + logDebugMessage(`Sending email verification email to ${emailInfo}`) + await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: 'EMAIL_VERIFICATION', + user: { + id: userId, + email: emailInfo.email, + }, + emailVerifyLink, + userContext, + }) + + return { + status: 'OK', + } + } + else { + throw new SessionError({ type: SessionError.UNAUTHORISED, message: 'Unknown User ID provided' }) + } + }, + } +} diff --git a/src/recipe/emailverification/constants.ts b/src/recipe/emailverification/constants.ts new file mode 100644 index 000000000..c44e7f481 --- /dev/null +++ b/src/recipe/emailverification/constants.ts @@ -0,0 +1,18 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const GENERATE_EMAIL_VERIFY_TOKEN_API = '/user/email/verify/token' + +export const EMAIL_VERIFY_API = '/user/email/verify' diff --git a/src/recipe/emailverification/emailVerificationClaim.ts b/src/recipe/emailverification/emailVerificationClaim.ts new file mode 100644 index 000000000..003d39b25 --- /dev/null +++ b/src/recipe/emailverification/emailVerificationClaim.ts @@ -0,0 +1,53 @@ +import { BooleanClaim } from '../session/claims' +import { SessionClaimValidator } from '../session' +import EmailVerificationRecipe from './recipe' + +/** + * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. + * */ +export class EmailVerificationClaimClass extends BooleanClaim { + constructor() { + super({ + key: 'st-ev', + async fetchValue(userId, userContext) { + const recipe = EmailVerificationRecipe.getInstanceOrThrowError() + const emailInfo = await recipe.getEmailForUserId(userId, userContext) + + if (emailInfo.status === 'OK') { + return recipe.recipeInterfaceImpl.isEmailVerified({ userId, email: emailInfo.email, userContext }) + } + else if (emailInfo.status === 'EMAIL_DOES_NOT_EXIST_ERROR') { + // We consider people without email addresses as validated + return true + } + else { + throw new Error('UNKNOWN_USER_ID') + } + }, + defaultMaxAgeInSeconds: 300, + }) + + this.validators = { + ...this.validators, + isVerified: (refetchTimeOnFalseInSeconds = 10, maxAgeInSeconds = 300) => ({ + ...this.validators.hasValue(true, maxAgeInSeconds), + shouldRefetch: (payload, userContext) => { + const value = this.getValueFromPayload(payload, userContext) + return ( + value === undefined + || this.getLastRefetchTime(payload, userContext)! < Date.now() - maxAgeInSeconds * 1000 + || (value === false + && this.getLastRefetchTime(payload, userContext)! + < Date.now() - refetchTimeOnFalseInSeconds * 1000) + ) + }, + }), + } + } + + validators!: BooleanClaim['validators'] & { + isVerified: (refetchTimeOnFalseInSeconds?: number, maxAgeInSeconds?: number) => SessionClaimValidator + } +} + +export const EmailVerificationClaim = new EmailVerificationClaimClass() diff --git a/src/recipe/emailverification/emailVerificationFunctions.ts b/src/recipe/emailverification/emailVerificationFunctions.ts new file mode 100644 index 000000000..edb10796a --- /dev/null +++ b/src/recipe/emailverification/emailVerificationFunctions.ts @@ -0,0 +1,70 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import axios, { AxiosError } from 'axios' +import { NormalisedAppinfo } from '../../types' +import { logDebugMessage } from '../../logger' +import { User } from './types' + +export function createAndSendCustomEmail(appInfo: NormalisedAppinfo) { + return async (user: User, emailVerifyURLWithToken: string) => { + if (process.env.TEST_MODE === 'testing') + return + + try { + await axios({ + method: 'POST', + url: 'https://api.supertokens.io/0/st/auth/email/verify', + data: { + email: user.email, + appName: appInfo.appName, + emailVerifyURL: emailVerifyURLWithToken, + }, + headers: { + 'api-version': 0, + }, + }) + logDebugMessage(`Email sent to ${user.email}`) + } + catch (error) { + logDebugMessage('Error sending verification email') + if (axios.isAxiosError(error)) { + const err = error as AxiosError + if (err.response) { + logDebugMessage(`Error status: ${err.response.status}`) + logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`) + } + else { + logDebugMessage(`Error: ${err.message}`) + } + } + else { + logDebugMessage(`Error: ${JSON.stringify(error)}`) + } + logDebugMessage('Logging the input below:') + logDebugMessage( + JSON.stringify( + { + email: user.email, + appName: appInfo.appName, + emailVerifyURL: emailVerifyURLWithToken, + }, + null, + 2, + ), + ) + } + } +} diff --git a/src/recipe/emailverification/emaildelivery/index.ts b/src/recipe/emailverification/emaildelivery/index.ts new file mode 100644 index 000000000..ef147a362 --- /dev/null +++ b/src/recipe/emailverification/emaildelivery/index.ts @@ -0,0 +1 @@ +export * from './services' diff --git a/src/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts b/src/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts new file mode 100644 index 000000000..d878efbd2 --- /dev/null +++ b/src/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts @@ -0,0 +1,59 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeEmailVerificationEmailDeliveryInput, User } from '../../../types' +import { createAndSendCustomEmail as defaultCreateAndSendCustomEmail } from '../../../emailVerificationFunctions' +import { NormalisedAppinfo } from '../../../../../types' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' + +export default class BackwardCompatibilityService +implements EmailDeliveryInterface { + private appInfo: NormalisedAppinfo + private isInServerlessEnv: boolean + private createAndSendCustomEmail: ( + user: User, + emailVerificationURLWithToken: string, + userContext: any + ) => Promise + + constructor( + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + createAndSendCustomEmail?: ( + user: User, + emailVerificationURLWithToken: string, + userContext: any + ) => Promise, + ) { + this.appInfo = appInfo + this.isInServerlessEnv = isInServerlessEnv + this.createAndSendCustomEmail + = createAndSendCustomEmail === undefined + ? defaultCreateAndSendCustomEmail(this.appInfo) + : createAndSendCustomEmail + } + + sendEmail = async (input: TypeEmailVerificationEmailDeliveryInput & { userContext: any }) => { + try { + if (!this.isInServerlessEnv) { + this.createAndSendCustomEmail(input.user, input.emailVerifyLink, input.userContext).catch((_) => {}) + } + else { + // see https://github.com/supertokens/supertokens-node/pull/135 + await this.createAndSendCustomEmail(input.user, input.emailVerifyLink, input.userContext) + } + } + catch (_) {} + } +} diff --git a/src/recipe/emailverification/emaildelivery/services/index.ts b/src/recipe/emailverification/emaildelivery/services/index.ts new file mode 100644 index 000000000..6e257c914 --- /dev/null +++ b/src/recipe/emailverification/emaildelivery/services/index.ts @@ -0,0 +1,17 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import SMTP from './smtp' +export const SMTPService = SMTP diff --git a/src/recipe/emailverification/emaildelivery/services/smtp/emailVerify.ts b/src/recipe/emailverification/emaildelivery/services/smtp/emailVerify.ts new file mode 100644 index 000000000..aaa50f91b --- /dev/null +++ b/src/recipe/emailverification/emaildelivery/services/smtp/emailVerify.ts @@ -0,0 +1,941 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeEmailVerificationEmailDeliveryInput } from '../../../types' +import { GetContentResult } from '../../../../../ingredients/emaildelivery/services/smtp' +import Supertokens from '../../../../../supertokens' + +export default function getEmailVerifyEmailContent(input: TypeEmailVerificationEmailDeliveryInput): GetContentResult { + const supertokens = Supertokens.getInstanceOrThrowError() + const appName = supertokens.appInfo.appName + const body = getEmailVerifyEmailHTML(appName, input.user.email, input.emailVerifyLink) + return { + body, + toEmail: input.user.email, + subject: 'Email verification instructions', + isHtml: true, + } +} + +export function getEmailVerifyEmailHTML(appName: string, email: string, verificationLink: string) { + return ` + + + + + + + + *|MC:SUBJECT|* + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + + + + +
+ + +
+
+ +

+ Please verify your email address for ${appName} + by clicking the button below.

+ + +
+
+

+ Alternatively, you can directly paste this link + in your browser
+ ${verificationLink} +

+
+
+ + + + +
+
+ +
+ + + + + +
+ + + + + + +
+ + +

+ This email is meant for ${email} +

+
+
+ +
+ +
+
+ + + + ` +} diff --git a/src/recipe/emailverification/emaildelivery/services/smtp/index.ts b/src/recipe/emailverification/emaildelivery/services/smtp/index.ts new file mode 100644 index 000000000..68e438c5f --- /dev/null +++ b/src/recipe/emailverification/emaildelivery/services/smtp/index.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { createTransport } from 'nodemailer' +import OverrideableBuilder from 'overrideableBuilder' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' +import { ServiceInterface, TypeInput } from '../../../../../ingredients/emaildelivery/services/smtp' +import { TypeEmailVerificationEmailDeliveryInput } from '../../../types' +import { getServiceImplementation } from './serviceImplementation' + +export default class SMTPService implements EmailDeliveryInterface { + serviceImpl: ServiceInterface + + constructor(config: TypeInput) { + const transporter = createTransport({ + host: config.smtpSettings.host, + port: config.smtpSettings.port, + auth: { + user: config.smtpSettings.authUsername || config.smtpSettings.from.email, + pass: config.smtpSettings.password, + }, + secure: config.smtpSettings.secure, + }) + let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)) + if (config.override !== undefined) + builder = builder.override(config.override) + + this.serviceImpl = builder.build() + } + + sendEmail = async (input: TypeEmailVerificationEmailDeliveryInput & { userContext: any }) => { + const content = await this.serviceImpl.getContent(input) + await this.serviceImpl.sendRawEmail({ + ...content, + userContext: input.userContext, + }) + } +} diff --git a/src/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts b/src/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts new file mode 100644 index 000000000..14637712f --- /dev/null +++ b/src/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts @@ -0,0 +1,57 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { Transporter } from 'nodemailer' +import { TypeEmailVerificationEmailDeliveryInput } from '../../../types' +import { + GetContentResult, + ServiceInterface, + TypeInputSendRawEmail, +} from '../../../../../ingredients/emaildelivery/services/smtp' +import getEmailVerifyEmailContent from './emailVerify' + +export function getServiceImplementation( + transporter: Transporter, + from: { + name: string + email: string + }, +): ServiceInterface { + return { + async sendRawEmail(input: TypeInputSendRawEmail) { + if (input.isHtml) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, + }) + } + else { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + text: input.body, + }) + } + }, + async getContent( + input: TypeEmailVerificationEmailDeliveryInput & { userContext: any }, + ): Promise { + return getEmailVerifyEmailContent(input) + }, + } +} diff --git a/src/recipe/emailverification/error.ts b/src/recipe/emailverification/error.ts new file mode 100644 index 000000000..a7174a414 --- /dev/null +++ b/src/recipe/emailverification/error.ts @@ -0,0 +1,25 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../../error' + +export default class SessionError extends STError { + constructor(options: { type: 'BAD_INPUT_ERROR'; message: string }) { + super({ + ...options, + }) + this.fromRecipe = 'emailverification' + } +} diff --git a/src/recipe/emailverification/index.ts b/src/recipe/emailverification/index.ts new file mode 100644 index 000000000..6e53031e1 --- /dev/null +++ b/src/recipe/emailverification/index.ts @@ -0,0 +1,172 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import Recipe from './recipe' +import SuperTokensError from './error' +import { APIInterface, APIOptions, RecipeInterface, TypeEmailVerificationEmailDeliveryInput, User } from './types' +import { EmailVerificationClaim } from './emailVerificationClaim' + +export default class Wrapper { + static init = Recipe.init + + static Error = SuperTokensError + + static EmailVerificationClaim = EmailVerificationClaim + + static async createEmailVerificationToken( + userId: string, + email?: string, + userContext?: any, + ): Promise< + | { + status: 'OK' + token: string + } + | { status: 'EMAIL_ALREADY_VERIFIED_ERROR' } + > { + const recipeInstance = Recipe.getInstanceOrThrowError() + + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext) + if (emailInfo.status === 'OK') { + email = emailInfo.email + } + else if (emailInfo.status === 'EMAIL_DOES_NOT_EXIST_ERROR') { + return { + status: 'EMAIL_ALREADY_VERIFIED_ERROR', + } + } + else { + throw new global.Error('Unknown User ID provided without email') + } + } + + return await recipeInstance.recipeInterfaceImpl.createEmailVerificationToken({ + userId, + email: email!, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async verifyEmailUsingToken(token: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ + token, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async isEmailVerified(userId: string, email?: string, userContext?: any) { + const recipeInstance = Recipe.getInstanceOrThrowError() + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext) + + if (emailInfo.status === 'OK') + email = emailInfo.email + else if (emailInfo.status === 'EMAIL_DOES_NOT_EXIST_ERROR') + return true + else + throw new global.Error('Unknown User ID provided without email') + } + + return await recipeInstance.recipeInterfaceImpl.isEmailVerified({ + userId, + email: email!, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async revokeEmailVerificationTokens(userId: string, email?: string, userContext?: any) { + const recipeInstance = Recipe.getInstanceOrThrowError() + + // If the dev wants to delete the tokens for an old email address of the user they can pass the address + // but redeeming those tokens would have no effect on isEmailVerified called without the old address + // so in general that is not necessary either. + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext) + if (emailInfo.status === 'OK') { + email = emailInfo.email + } + else if (emailInfo.status === 'EMAIL_DOES_NOT_EXIST_ERROR') { + // This only happens for phone based passwordless users (or if the user added a custom getEmailForUserId) + // We can return OK here, since there is no way to create an email verification token + // if getEmailForUserId returns EMAIL_DOES_NOT_EXIST_ERROR. + return { + status: 'OK', + } + } + else { + throw new global.Error('Unknown User ID provided without email') + } + } + + return await recipeInstance.recipeInterfaceImpl.revokeEmailVerificationTokens({ + userId, + email: email!, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async unverifyEmail(userId: string, email?: string, userContext?: any) { + const recipeInstance = Recipe.getInstanceOrThrowError() + if (email === undefined) { + const emailInfo = await recipeInstance.getEmailForUserId(userId, userContext) + if (emailInfo.status === 'OK') { + email = emailInfo.email + } + else if (emailInfo.status === 'EMAIL_DOES_NOT_EXIST_ERROR') { + // Here we are returning OK since that's how it used to work, but a later call to isVerified will still return true + return { + status: 'OK', + } + } + else { + throw new global.Error('Unknown User ID provided without email') + } + } + return await recipeInstance.recipeInterfaceImpl.unverifyEmail({ + userId, + email: email!, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: any }) { + const recipeInstance = Recipe.getInstanceOrThrowError() + return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail({ + userContext: {}, + ...input, + }) + } +} + +export const init = Wrapper.init + +export const Error = Wrapper.Error + +export const createEmailVerificationToken = Wrapper.createEmailVerificationToken + +export const verifyEmailUsingToken = Wrapper.verifyEmailUsingToken + +export const isEmailVerified = Wrapper.isEmailVerified + +export const revokeEmailVerificationTokens = Wrapper.revokeEmailVerificationTokens + +export const unverifyEmail = Wrapper.unverifyEmail + +export type { RecipeInterface, APIOptions, APIInterface, User } + +export const sendEmail = Wrapper.sendEmail + +export { EmailVerificationClaim } from './emailVerificationClaim' diff --git a/src/recipe/emailverification/recipe.ts b/src/recipe/emailverification/recipe.ts new file mode 100644 index 000000000..867291055 --- /dev/null +++ b/src/recipe/emailverification/recipe.ts @@ -0,0 +1,210 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import { PostSuperTokensInitCallbacks } from '../../postSuperTokensInitCallbacks' +import SessionRecipe from '../session/recipe' +import { APIInterface, GetEmailForUserIdFunc, RecipeInterface, TypeEmailVerificationEmailDeliveryInput, TypeInput, TypeNormalisedInput } from './types' +import STError from './error' +import { validateAndNormaliseUserInput } from './utils' +import { EMAIL_VERIFY_API, GENERATE_EMAIL_VERIFY_TOKEN_API } from './constants' +import generateEmailVerifyTokenAPI from './api/generateEmailVerifyToken' +import emailVerifyAPI from './api/emailVerify' +import RecipeImplementation from './recipeImplementation' +import APIImplementation from './api/implementation' +import { EmailVerificationClaim } from './emailVerificationClaim' + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined + static RECIPE_ID = 'emailverification' + + config: TypeNormalisedInput + + recipeInterfaceImpl: RecipeInterface + + apiImpl: APIInterface + + isInServerlessEnv: boolean + + emailDelivery: EmailDeliveryIngredient + + getEmailForUserIdFuncsFromOtherRecipes: GetEmailForUserIdFunc[] = [] + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + ingredients: { + emailDelivery: EmailDeliveryIngredient | undefined + }, + ) { + super(recipeId, appInfo) + this.config = validateAndNormaliseUserInput(this, appInfo, config) + this.isInServerlessEnv = isInServerlessEnv + + { + const builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + + /** + * emailDelivery will always needs to be declared after isInServerlessEnv + * and recipeInterfaceImpl values are set + */ + this.emailDelivery + = ingredients.emailDelivery === undefined + ? new EmailDeliveryIngredient(this.config.getEmailDeliveryConfig(this.isInServerlessEnv)) + : ingredients.emailDelivery + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static getInstance(): Recipe | undefined { + return Recipe.instance + } + + static init(config: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { + emailDelivery: undefined, + }) + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(EmailVerificationClaim) + + if (config.mode === 'REQUIRED') { + SessionRecipe.getInstanceOrThrowError().addClaimValidatorFromOtherRecipe( + EmailVerificationClaim.validators.isVerified(), + ) + } + }) + + return Recipe.instance + } + else { + throw new Error( + 'Emailverification recipe has already been initialised. Please check your code for bugs.', + ) + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + // abstract instance functions below............... + + getAPIsHandled = (): APIHandled[] => { + return [ + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(GENERATE_EMAIL_VERIFY_TOKEN_API), + id: GENERATE_EMAIL_VERIFY_TOKEN_API, + disabled: this.apiImpl.generateEmailVerifyTokenPOST === undefined, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(EMAIL_VERIFY_API), + id: EMAIL_VERIFY_API, + disabled: this.apiImpl.verifyEmailPOST === undefined, + }, + { + method: 'get', + pathWithoutApiBasePath: new NormalisedURLPath(EMAIL_VERIFY_API), + id: EMAIL_VERIFY_API, + disabled: this.apiImpl.isEmailVerifiedGET === undefined, + }, + ] + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + _: NormalisedURLPath, + __: HTTPMethod, + ): Promise => { + const options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + emailDelivery: this.emailDelivery, + appInfo: this.getAppInfo(), + } + if (id === GENERATE_EMAIL_VERIFY_TOKEN_API) + return await generateEmailVerifyTokenAPI(this.apiImpl, options) + else + return await emailVerifyAPI(this.apiImpl, options) + } + + handleError = async (err: STError, _: BaseRequest, __: BaseResponse): Promise => { + throw err + } + + getAllCORSHeaders = (): string[] => { + return [] + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } + + getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { + if (this.config.getEmailForUserId !== undefined) { + const userRes = await this.config.getEmailForUserId(userId, userContext) + if (userRes.status !== 'UNKNOWN_USER_ID_ERROR') + return userRes + } + + for (const getEmailForUserId of this.getEmailForUserIdFuncsFromOtherRecipes) { + const res = await getEmailForUserId(userId, userContext) + if (res.status !== 'UNKNOWN_USER_ID_ERROR') + return res + } + + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } + + addGetEmailForUserIdFunc = (func: GetEmailForUserIdFunc): void => { + this.getEmailForUserIdFuncsFromOtherRecipes.push(func) + } +} diff --git a/src/recipe/emailverification/recipeImplementation.ts b/src/recipe/emailverification/recipeImplementation.ts new file mode 100644 index 000000000..36be130dd --- /dev/null +++ b/src/recipe/emailverification/recipeImplementation.ts @@ -0,0 +1,89 @@ +import { Querier } from '../../querier' +import NormalisedURLPath from '../../normalisedURLPath' +import { RecipeInterface, User } from './' + +export default function getRecipeInterface(querier: Querier): RecipeInterface { + return { + async createEmailVerificationToken({ + userId, + email, + }: { + userId: string + email: string + }): Promise< + | { + status: 'OK' + token: string + } + | { status: 'EMAIL_ALREADY_VERIFIED_ERROR' } + > { + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/user/email/verify/token'), { + userId, + email, + }) + if (response.status === 'OK') { + return { + status: 'OK', + token: response.token, + } + } + else { + return { + status: 'EMAIL_ALREADY_VERIFIED_ERROR', + } + } + }, + + async verifyEmailUsingToken({ + token, + }: { + token: string + }): Promise<{ status: 'OK'; user: User } | { status: 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR' }> { + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/user/email/verify'), { + method: 'token', + token, + }) + if (response.status === 'OK') { + return { + status: 'OK', + user: { + id: response.userId, + email: response.email, + }, + } + } + else { + return { + status: 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR', + } + } + }, + + async isEmailVerified({ userId, email }: { userId: string; email: string }): Promise { + const response = await querier.sendGetRequest(new NormalisedURLPath('/recipe/user/email/verify'), { + userId, + email, + }) + return response.isVerified + }, + + async revokeEmailVerificationTokens(input: { + userId: string + email: string + }): Promise<{ status: 'OK' }> { + await querier.sendPostRequest(new NormalisedURLPath('/recipe/user/email/verify/token/remove'), { + userId: input.userId, + email: input.email, + }) + return { status: 'OK' } + }, + + async unverifyEmail(input: { userId: string; email: string }): Promise<{ status: 'OK' }> { + await querier.sendPostRequest(new NormalisedURLPath('/recipe/user/email/verify/remove'), { + userId: input.userId, + email: input.email, + }) + return { status: 'OK' } + }, + } +} diff --git a/src/recipe/emailverification/types.ts b/src/recipe/emailverification/types.ts new file mode 100644 index 000000000..aab164df2 --- /dev/null +++ b/src/recipe/emailverification/types.ts @@ -0,0 +1,175 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { + TypeInput as EmailDeliveryTypeInput, + TypeInputWithService as EmailDeliveryTypeInputWithService, +} from '../../ingredients/emaildelivery/types' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import { GeneralErrorResponse, NormalisedAppinfo } from '../../types' +import { SessionContainerInterface } from '../session/types' + +export interface TypeInput { + mode: 'REQUIRED' | 'OPTIONAL' + emailDelivery?: EmailDeliveryTypeInput + getEmailForUserId?: ( + userId: string, + userContext: any + ) => Promise< + | { + status: 'OK' + email: string + } + | { status: 'EMAIL_DOES_NOT_EXIST_ERROR' | 'UNKNOWN_USER_ID_ERROR' } + > + /** + * @deprecated Please use emailDelivery config instead + */ + createAndSendCustomEmail?: (user: User, emailVerificationURLWithToken: string, userContext: any) => Promise + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeNormalisedInput { + mode: 'REQUIRED' | 'OPTIONAL' + getEmailDeliveryConfig: ( + isInServerlessEnv: boolean + ) => EmailDeliveryTypeInputWithService + getEmailForUserId?: ( + userId: string, + userContext: any + ) => Promise< + | { + status: 'OK' + email: string + } + | { status: 'EMAIL_DOES_NOT_EXIST_ERROR' | 'UNKNOWN_USER_ID_ERROR' } + > + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface User { + id: string + email: string +} + +export interface RecipeInterface { + createEmailVerificationToken(input: { + userId: string + email: string + userContext: any + }): Promise< + | { + status: 'OK' + token: string + } + | { status: 'EMAIL_ALREADY_VERIFIED_ERROR' } + > + + verifyEmailUsingToken(input: { + token: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR' }> + + isEmailVerified(input: { userId: string; email: string; userContext: any }): Promise + + revokeEmailVerificationTokens(input: { + userId: string + email: string + userContext: any + }): Promise<{ status: 'OK' }> + + unverifyEmail(input: { userId: string; email: string; userContext: any }): Promise<{ status: 'OK' }> +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + appInfo: NormalisedAppinfo + config: TypeNormalisedInput + recipeId: string + isInServerlessEnv: boolean + req: BaseRequest + res: BaseResponse + emailDelivery: EmailDeliveryIngredient +} + +export interface APIInterface { + verifyEmailPOST: + | undefined + | ((input: { + token: string + options: APIOptions + userContext: any + session?: SessionContainerInterface + }) => Promise< + { status: 'OK'; user: User } | { status: 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR' } | GeneralErrorResponse + >) + + isEmailVerifiedGET: + | undefined + | ((input: { + options: APIOptions + userContext: any + session: SessionContainerInterface + }) => Promise< + | { + status: 'OK' + isVerified: boolean + } + | GeneralErrorResponse + >) + + generateEmailVerifyTokenPOST: + | undefined + | ((input: { + options: APIOptions + userContext: any + session: SessionContainerInterface + }) => Promise<{ status: 'EMAIL_ALREADY_VERIFIED_ERROR' | 'OK' } | GeneralErrorResponse>) +} + +export interface TypeEmailVerificationEmailDeliveryInput { + type: 'EMAIL_VERIFICATION' + user: { + id: string + email: string + } + emailVerifyLink: string +} + +export type GetEmailForUserIdFunc = ( + userId: string, + userContext: any +) => Promise< + | { + status: 'OK' + email: string + } + | { status: 'EMAIL_DOES_NOT_EXIST_ERROR' | 'UNKNOWN_USER_ID_ERROR' } +> diff --git a/src/recipe/emailverification/utils.ts b/src/recipe/emailverification/utils.ts new file mode 100644 index 000000000..75de51b8e --- /dev/null +++ b/src/recipe/emailverification/utils.ts @@ -0,0 +1,82 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import Recipe from './recipe' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import BackwardCompatibilityService from './emaildelivery/services/backwardCompatibility' + +export function validateAndNormaliseUserInput( + _: Recipe, + appInfo: NormalisedAppinfo, + config: TypeInput, +): TypeNormalisedInput { + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config.override, + } + + function getEmailDeliveryConfig(isInServerlessEnv: boolean) { + let emailService = config.emailDelivery?.service + /** + * following code is for backward compatibility. + * if user has not passed emailDelivery config, we + * use the createAndSendCustomEmail config. If the user + * has not passed even that config, we use the default + * createAndSendCustomEmail implementation which calls our supertokens API + */ + if (emailService === undefined) { + emailService = new BackwardCompatibilityService( + appInfo, + isInServerlessEnv, + config.createAndSendCustomEmail, + ) + } + return { + ...config.emailDelivery, + /** + * if we do + * let emailDelivery = { + * service: emailService, + * ...config.emailDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + service: emailService, + } + } + return { + mode: config.mode, + getEmailForUserId: config.getEmailForUserId, + override, + getEmailDeliveryConfig, + } +} + +export function getEmailVerifyLink(input: { appInfo: NormalisedAppinfo; token: string; recipeId: string }): string { + return ( + `${input.appInfo.websiteDomain.getAsStringDangerous() + + input.appInfo.websiteBasePath.getAsStringDangerous() + }/verify-email` + + `?token=${ + input.token + }&rid=${ + input.recipeId}` + ) +} diff --git a/src/recipe/jwt/api/getJWKS.ts b/src/recipe/jwt/api/getJWKS.ts new file mode 100644 index 000000000..bc58875e1 --- /dev/null +++ b/src/recipe/jwt/api/getJWKS.ts @@ -0,0 +1,35 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../types' + +export default async function getJWKS(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.getJWKSGET === undefined) + return false + + const result = await apiImplementation.getJWKSGET({ + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + if (result.status === 'OK') { + options.res.setHeader('Access-Control-Allow-Origin', '*', false) + send200Response(options.res, { keys: result.keys }) + } + else { + send200Response(options.res, result) + } + return true +} diff --git a/src/recipe/jwt/api/implementation.ts b/src/recipe/jwt/api/implementation.ts new file mode 100644 index 000000000..03fcc16af --- /dev/null +++ b/src/recipe/jwt/api/implementation.ts @@ -0,0 +1,31 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { APIInterface, APIOptions, JsonWebKey } from '../types' +import { GeneralErrorResponse } from '../../../types' + +export default function getAPIImplementation(): APIInterface { + return { + async getJWKSGET({ + options, + userContext, + }: { + options: APIOptions + userContext: any + }): Promise<{ status: 'OK'; keys: JsonWebKey[] } | GeneralErrorResponse> { + return await options.recipeImplementation.getJWKS({ userContext }) + }, + } +} diff --git a/src/recipe/jwt/constants.ts b/src/recipe/jwt/constants.ts new file mode 100644 index 000000000..42563415f --- /dev/null +++ b/src/recipe/jwt/constants.ts @@ -0,0 +1,16 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const GET_JWKS_API = '/jwt/jwks.json' diff --git a/src/recipe/jwt/index.ts b/src/recipe/jwt/index.ts new file mode 100644 index 000000000..f49bf410f --- /dev/null +++ b/src/recipe/jwt/index.ts @@ -0,0 +1,41 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import Recipe from './recipe' +import { APIInterface, APIOptions, JsonWebKey, RecipeInterface } from './types' + +export default class Wrapper { + static init = Recipe.init + + static async createJWT(payload: any, validitySeconds?: number, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ + payload, + validitySeconds, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async getJWKS(userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ + userContext: userContext === undefined ? {} : userContext, + }) + } +} + +export const init = Wrapper.init +export const createJWT = Wrapper.createJWT +export const getJWKS = Wrapper.getJWKS + +export type { APIInterface, APIOptions, RecipeInterface, JsonWebKey } diff --git a/src/recipe/jwt/recipe.ts b/src/recipe/jwt/recipe.ts new file mode 100644 index 000000000..d3c98c567 --- /dev/null +++ b/src/recipe/jwt/recipe.ts @@ -0,0 +1,128 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import SuperTokensError from '../../error' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import getJWKS from './api/getJWKS' +import APIImplementation from './api/implementation' +import { GET_JWKS_API } from './constants' +import RecipeImplementation from './recipeImplementation' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import { validateAndNormaliseUserInput } from './utils' + +export default class Recipe extends RecipeModule { + static RECIPE_ID = 'jwt' + private static instance: Recipe | undefined = undefined + + config: TypeNormalisedInput + recipeInterfaceImpl: RecipeInterface + apiImpl: APIInterface + isInServerlessEnv: boolean + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo) + this.config = validateAndNormaliseUserInput(this, appInfo, config) + this.isInServerlessEnv = isInServerlessEnv + + { + const builder = new OverrideableBuilder( + RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId), this.config, appInfo), + ) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + } + + /* Init functions */ + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config) + return Recipe.instance + } + else { + throw new Error('JWT recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + /* RecipeModule functions */ + + getAPIsHandled(): APIHandled[] { + return [ + { + method: 'get', + pathWithoutApiBasePath: new NormalisedURLPath(GET_JWKS_API), + id: GET_JWKS_API, + disabled: this.apiImpl.getJWKSGET === undefined, + }, + ] + } + + handleAPIRequest = async ( + _: string, + req: BaseRequest, + res: BaseResponse, + __: NormalisedURLPath, + ___: HTTPMethod, + ): Promise => { + const options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + } + + return await getJWKS(this.apiImpl, options) + } + + handleError(error: SuperTokensError, _: BaseRequest, __: BaseResponse): Promise { + throw error + } + + getAllCORSHeaders(): string[] { + return [] + } + + isErrorFromThisRecipe(err: any): err is SuperTokensError { + return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } +} diff --git a/src/recipe/jwt/recipeImplementation.ts b/src/recipe/jwt/recipeImplementation.ts new file mode 100644 index 000000000..f5be76ddb --- /dev/null +++ b/src/recipe/jwt/recipeImplementation.ts @@ -0,0 +1,71 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import { NormalisedAppinfo } from '../../types' +import { JsonWebKey, RecipeInterface, TypeNormalisedInput } from './types' + +export default function getRecipeInterface( + querier: Querier, + config: TypeNormalisedInput, + appInfo: NormalisedAppinfo, +): RecipeInterface { + return { + async createJWT({ + payload, + validitySeconds, + }: { + payload?: any + validitySeconds?: number + }): Promise< + | { + status: 'OK' + jwt: string + } + | { + status: 'UNSUPPORTED_ALGORITHM_ERROR' + } + > { + if (validitySeconds === undefined) { + // If the user does not provide a validity to this function and the config validity is also undefined, use 100 years (in seconds) + validitySeconds = config.jwtValiditySeconds + } + + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/jwt'), { + payload: payload ?? {}, + validity: validitySeconds, + algorithm: 'RS256', + jwksDomain: appInfo.apiDomain.getAsStringDangerous(), + }) + + if (response.status === 'OK') { + return { + status: 'OK', + jwt: response.jwt, + } + } + else { + return { + status: 'UNSUPPORTED_ALGORITHM_ERROR', + } + } + }, + + async getJWKS(): Promise<{ status: 'OK'; keys: JsonWebKey[] }> { + return await querier.sendGetRequest(new NormalisedURLPath('/recipe/jwt/jwks'), {}) + }, + } +} diff --git a/src/recipe/jwt/types.ts b/src/recipe/jwt/types.ts new file mode 100644 index 000000000..a8ea8e53d --- /dev/null +++ b/src/recipe/jwt/types.ts @@ -0,0 +1,91 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { GeneralErrorResponse } from '../../types' + +export interface JsonWebKey { + kty: string + kid: string + n: string + e: string + alg: string + use: string +} + +export interface TypeInput { + jwtValiditySeconds?: number + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeNormalisedInput { + jwtValiditySeconds: number + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + config: TypeNormalisedInput + recipeId: string + isInServerlessEnv: boolean + req: BaseRequest + res: BaseResponse +} + +export interface RecipeInterface { + createJWT(input: { + payload?: any + validitySeconds?: number + userContext: any + }): Promise< + | { + status: 'OK' + jwt: string + } + | { + status: 'UNSUPPORTED_ALGORITHM_ERROR' + } + > + + getJWKS(input: { + userContext: any + }): Promise<{ + status: 'OK' + keys: JsonWebKey[] + }> +} + +export interface APIInterface { + getJWKSGET: + | undefined + | ((input: { + options: APIOptions + userContext: any + }) => Promise<{ status: 'OK'; keys: JsonWebKey[] } | GeneralErrorResponse>) +} diff --git a/src/recipe/jwt/utils.ts b/src/recipe/jwt/utils.ts new file mode 100644 index 000000000..34608455c --- /dev/null +++ b/src/recipe/jwt/utils.ts @@ -0,0 +1,35 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import Recipe from './recipe' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' + +export function validateAndNormaliseUserInput( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInput, +): TypeNormalisedInput { + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + return { + jwtValiditySeconds: config?.jwtValiditySeconds ?? 3153600000, + override, + } +} diff --git a/src/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts b/src/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts new file mode 100644 index 000000000..2bf3a1a2b --- /dev/null +++ b/src/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts @@ -0,0 +1,40 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { APIInterface, APIOptions } from '../types' +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' + +export default async function getOpenIdDiscoveryConfiguration( + apiImplementation: APIInterface, + options: APIOptions, +): Promise { + if (apiImplementation.getOpenIdDiscoveryConfigurationGET === undefined) + return false + + const result = await apiImplementation.getOpenIdDiscoveryConfigurationGET({ + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + if (result.status === 'OK') { + options.res.setHeader('Access-Control-Allow-Origin', '*', false) + send200Response(options.res, { + issuer: result.issuer, + jwks_uri: result.jwks_uri, + }) + } + else { + send200Response(options.res, result) + } + return true +} diff --git a/src/recipe/openid/api/implementation.ts b/src/recipe/openid/api/implementation.ts new file mode 100644 index 000000000..400177183 --- /dev/null +++ b/src/recipe/openid/api/implementation.ts @@ -0,0 +1,30 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { APIInterface, APIOptions } from '../types' +import { GeneralErrorResponse } from '../../../types' + +export default function getAPIImplementation(): APIInterface { + return { + async getOpenIdDiscoveryConfigurationGET({ + options, + userContext, + }: { + options: APIOptions + userContext: any + }): Promise<{ status: 'OK'; issuer: string; jwks_uri: string } | GeneralErrorResponse> { + return await options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }) + }, + } +} diff --git a/src/recipe/openid/constants.ts b/src/recipe/openid/constants.ts new file mode 100644 index 000000000..327426484 --- /dev/null +++ b/src/recipe/openid/constants.ts @@ -0,0 +1,15 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +export const GET_DISCOVERY_CONFIG_URL = '/.well-known/openid-configuration' diff --git a/src/recipe/openid/index.ts b/src/recipe/openid/index.ts new file mode 100644 index 000000000..e559814eb --- /dev/null +++ b/src/recipe/openid/index.ts @@ -0,0 +1,30 @@ +import OpenIdRecipe from './recipe' + +export default class OpenIdRecipeWrapper { + static init = OpenIdRecipe.init + + static getOpenIdDiscoveryConfiguration(userContext?: any) { + return OpenIdRecipe.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ + userContext: userContext === undefined ? {} : userContext, + }) + } + + static createJWT(payload?: any, validitySeconds?: number, userContext?: any) { + return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.createJWT({ + payload, + validitySeconds, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static getJWKS(userContext?: any) { + return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.getJWKS({ + userContext: userContext === undefined ? {} : userContext, + }) + } +} + +export const init = OpenIdRecipeWrapper.init +export const getOpenIdDiscoveryConfiguration = OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration +export const createJWT = OpenIdRecipeWrapper.createJWT +export const getJWKS = OpenIdRecipeWrapper.getJWKS diff --git a/src/recipe/openid/recipe.ts b/src/recipe/openid/recipe.ts new file mode 100644 index 000000000..bbc57bc7e --- /dev/null +++ b/src/recipe/openid/recipe.ts @@ -0,0 +1,134 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import STError from '../../error' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import NormalisedURLPath from '../../normalisedURLPath' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import JWTRecipe from '../jwt/recipe' +import { APIInterface, APIOptions, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import { validateAndNormaliseUserInput } from './utils' +import RecipeImplementation from './recipeImplementation' +import APIImplementation from './api/implementation' +import { GET_DISCOVERY_CONFIG_URL } from './constants' +import getOpenIdDiscoveryConfiguration from './api/getOpenIdDiscoveryConfiguration' + +export class OpenIdRecipe extends RecipeModule { + static RECIPE_ID = 'openid' + private static instance: OpenIdRecipe | undefined = undefined + config: TypeNormalisedInput + jwtRecipe: JWTRecipe + recipeImplementation: RecipeInterface + apiImpl: APIInterface + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo) + + this.config = validateAndNormaliseUserInput(appInfo, config) + this.jwtRecipe = new JWTRecipe(recipeId, appInfo, isInServerlessEnv, { + jwtValiditySeconds: this.config.jwtValiditySeconds, + override: this.config.override.jwtFeature, + }) + + const builder = new OverrideableBuilder(RecipeImplementation(this.config, this.jwtRecipe.recipeInterfaceImpl)) + + this.recipeImplementation = builder.override(this.config.override.functions).build() + + const apiBuilder = new OverrideableBuilder(APIImplementation()) + + this.apiImpl = apiBuilder.override(this.config.override.apis).build() + } + + static getInstanceOrThrowError(): OpenIdRecipe { + if (OpenIdRecipe.instance !== undefined) + return OpenIdRecipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (OpenIdRecipe.instance === undefined) { + OpenIdRecipe.instance = new OpenIdRecipe(OpenIdRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config) + return OpenIdRecipe.instance + } + else { + throw new Error('OpenId recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + OpenIdRecipe.instance = undefined + } + + getAPIsHandled = (): APIHandled[] => { + return [ + { + method: 'get', + pathWithoutApiBasePath: new NormalisedURLPath(GET_DISCOVERY_CONFIG_URL), + id: GET_DISCOVERY_CONFIG_URL, + disabled: this.apiImpl.getOpenIdDiscoveryConfigurationGET === undefined, + }, + ...this.jwtRecipe.getAPIsHandled(), + ] + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + response: BaseResponse, + path: NormalisedURLPath, + method: HTTPMethod, + ): Promise => { + const apiOptions: APIOptions = { + recipeImplementation: this.recipeImplementation, + config: this.config, + recipeId: this.getRecipeId(), + req, + res: response, + } + + if (id === GET_DISCOVERY_CONFIG_URL) + return await getOpenIdDiscoveryConfiguration(this.apiImpl, apiOptions) + else + return this.jwtRecipe.handleAPIRequest(id, req, response, path, method) + } + + handleError = async (error: STError, request: BaseRequest, response: BaseResponse): Promise => { + if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) + throw error + else + return await this.jwtRecipe.handleError(error, request, response) + } + + getAllCORSHeaders = (): string[] => { + return [...this.jwtRecipe.getAllCORSHeaders()] + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return ( + (STError.isErrorFromSuperTokens(err) && err.fromRecipe === OpenIdRecipe.RECIPE_ID) + || this.jwtRecipe.isErrorFromThisRecipe(err) + ) + } +} + +export default OpenIdRecipe diff --git a/src/recipe/openid/recipeImplementation.ts b/src/recipe/openid/recipeImplementation.ts new file mode 100644 index 000000000..1bf74f838 --- /dev/null +++ b/src/recipe/openid/recipeImplementation.ts @@ -0,0 +1,73 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { RecipeInterface as JWTRecipeInterface, JsonWebKey } from '../jwt/types' +import NormalisedURLPath from '../../normalisedURLPath' +import { GET_JWKS_API } from '../jwt/constants' +import { RecipeInterface, TypeNormalisedInput } from './types' + +export default function getRecipeInterface( + config: TypeNormalisedInput, + jwtRecipeImplementation: JWTRecipeInterface, +): RecipeInterface { + return { + async getOpenIdDiscoveryConfiguration(): Promise<{ + status: 'OK' + issuer: string + jwks_uri: string + }> { + const issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous() + const jwks_uri + = config.issuerDomain.getAsStringDangerous() + + config.issuerPath.appendPath(new NormalisedURLPath(GET_JWKS_API)).getAsStringDangerous() + return { + status: 'OK', + issuer, + jwks_uri, + } + }, + async createJWT({ + payload, + validitySeconds, + userContext, + }: { + payload?: any + validitySeconds?: number + userContext: any + }): Promise< + | { + status: 'OK' + jwt: string + } + | { + status: 'UNSUPPORTED_ALGORITHM_ERROR' + } + > { + payload = (payload === undefined || payload === null) ? {} : payload + + const issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous() + return await jwtRecipeImplementation.createJWT({ + payload: { + iss: issuer, + ...payload, + }, + validitySeconds, + userContext, + }) + }, + async getJWKS(input): Promise<{ status: 'OK'; keys: JsonWebKey[] }> { + return await jwtRecipeImplementation.getJWKS(input) + }, + } +} diff --git a/src/recipe/openid/types.ts b/src/recipe/openid/types.ts new file mode 100644 index 000000000..eacc242cf --- /dev/null +++ b/src/recipe/openid/types.ts @@ -0,0 +1,120 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import NormalisedURLDomain from '../../normalisedURLDomain' +import NormalisedURLPath from '../../normalisedURLPath' +import { APIInterface as JWTAPIInterface, RecipeInterface as JWTRecipeInterface, JsonWebKey } from '../jwt/types' +import { GeneralErrorResponse } from '../../types' + +export interface TypeInput { + issuer?: string + jwtValiditySeconds?: number + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + jwtFeature?: { + functions?: ( + originalImplementation: JWTRecipeInterface, + builder?: OverrideableBuilder + ) => JWTRecipeInterface + apis?: ( + originalImplementation: JWTAPIInterface, + builder?: OverrideableBuilder + ) => JWTAPIInterface + } + } +} + +export interface TypeNormalisedInput { + issuerDomain: NormalisedURLDomain + issuerPath: NormalisedURLPath + jwtValiditySeconds?: number + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + jwtFeature?: { + functions?: ( + originalImplementation: JWTRecipeInterface, + builder?: OverrideableBuilder + ) => JWTRecipeInterface + apis?: ( + originalImplementation: JWTAPIInterface, + builder?: OverrideableBuilder + ) => JWTAPIInterface + } + } +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + config: TypeNormalisedInput + recipeId: string + req: BaseRequest + res: BaseResponse +} + +export interface APIInterface { + getOpenIdDiscoveryConfigurationGET: + | undefined + | ((input: { + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + issuer: string + jwks_uri: string + } + | GeneralErrorResponse + >) +} + +export interface RecipeInterface { + getOpenIdDiscoveryConfiguration(input: { + userContext: any + }): Promise<{ + status: 'OK' + issuer: string + jwks_uri: string + }> + createJWT(input: { + payload?: any + validitySeconds?: number + userContext: any + }): Promise< + | { + status: 'OK' + jwt: string + } + | { + status: 'UNSUPPORTED_ALGORITHM_ERROR' + } + > + + getJWKS(input: { + userContext: any + }): Promise<{ + status: 'OK' + keys: JsonWebKey[] + }> +} diff --git a/src/recipe/openid/utils.ts b/src/recipe/openid/utils.ts new file mode 100644 index 000000000..349448259 --- /dev/null +++ b/src/recipe/openid/utils.ts @@ -0,0 +1,46 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import NormalisedURLDomain from '../../normalisedURLDomain' +import NormalisedURLPath from '../../normalisedURLPath' +import { NormalisedAppinfo } from '../../types' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' + +export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config?: TypeInput): TypeNormalisedInput { + let issuerDomain = appInfo.apiDomain + let issuerPath = appInfo.apiBasePath + + if (config !== undefined) { + if (config.issuer !== undefined) { + issuerDomain = new NormalisedURLDomain(config.issuer) + issuerPath = new NormalisedURLPath(config.issuer) + } + + if (!issuerPath.equals(appInfo.apiBasePath)) + throw new Error('The path of the issuer URL must be equal to the apiBasePath. The default value is /auth') + } + + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + return { + issuerDomain, + issuerPath, + jwtValiditySeconds: config?.jwtValiditySeconds, + override, + } +} diff --git a/src/recipe/passwordless/api/consumeCode.ts b/src/recipe/passwordless/api/consumeCode.ts new file mode 100644 index 000000000..a41f39c5c --- /dev/null +++ b/src/recipe/passwordless/api/consumeCode.ts @@ -0,0 +1,81 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '..' + +export default async function consumeCode(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.consumeCodePOST === undefined) + return false + + const body = await options.req.getJSONBody() + const preAuthSessionId = body.preAuthSessionId + const linkCode = body.linkCode + const deviceId = body.deviceId + const userInputCode = body.userInputCode + + if (preAuthSessionId === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide preAuthSessionId', + }) + } + + if (deviceId !== undefined || userInputCode !== undefined) { + if (linkCode !== undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide one of (linkCode) or (deviceId+userInputCode) and not both', + }) + } + if (deviceId === undefined || userInputCode === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide both deviceId and userInputCode', + }) + } + } + else if (linkCode === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide one of (linkCode) or (deviceId+userInputCode) and not both', + }) + } + + const userContext = makeDefaultUserContextFromAPI(options.req) + const result = await apiImplementation.consumeCodePOST( + deviceId !== undefined + ? { + deviceId, + userInputCode, + preAuthSessionId, + options, + userContext, + } + : { + linkCode, + options, + preAuthSessionId, + userContext, + }, + ) + + if (result.status === 'OK') + delete (result as any).session + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/passwordless/api/createCode.ts b/src/recipe/passwordless/api/createCode.ts new file mode 100644 index 000000000..246adc2c1 --- /dev/null +++ b/src/recipe/passwordless/api/createCode.ts @@ -0,0 +1,97 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import parsePhoneNumber from 'libphonenumber-js/max' +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '..' + +export default async function createCode(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.createCodePOST === undefined) + return false + + const body = await options.req.getJSONBody() + let email: string | undefined = body.email + let phoneNumber: string | undefined = body.phoneNumber + + if ((email !== undefined && phoneNumber !== undefined) || (email === undefined && phoneNumber === undefined)) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide exactly one of email or phoneNumber', + }) + } + + if (email === undefined && options.config.contactMethod === 'EMAIL') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide an email since you have set the contactMethod to "EMAIL"', + }) + } + + if (phoneNumber === undefined && options.config.contactMethod === 'PHONE') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide a phoneNumber since you have set the contactMethod to "PHONE"', + }) + } + + // normalise and validate format of input + if ( + email !== undefined + && (options.config.contactMethod === 'EMAIL' || options.config.contactMethod === 'EMAIL_OR_PHONE') + ) { + email = email.trim() + const validateError = await options.config.validateEmailAddress(email) + if (validateError !== undefined) { + send200Response(options.res, { + status: 'GENERAL_ERROR', + message: validateError, + }) + return true + } + } + + if ( + phoneNumber !== undefined + && (options.config.contactMethod === 'PHONE' || options.config.contactMethod === 'EMAIL_OR_PHONE') + ) { + const validateError = await options.config.validatePhoneNumber(phoneNumber) + if (validateError !== undefined) { + send200Response(options.res, { + status: 'GENERAL_ERROR', + message: validateError, + }) + return true + } + const parsedPhoneNumber = parsePhoneNumber(phoneNumber) + if (parsedPhoneNumber === undefined) { + // this can come here if the user has provided their own impl of validatePhoneNumber and + // the phone number is valid according to their impl, but not according to the libphonenumber-js lib. + phoneNumber = phoneNumber.trim() + } + else { + phoneNumber = parsedPhoneNumber.format('E.164') + } + } + + const result = await apiImplementation.createCodePOST( + email !== undefined + ? { email, options, userContext: makeDefaultUserContextFromAPI(options.req) } + : { phoneNumber: phoneNumber!, options, userContext: makeDefaultUserContextFromAPI(options.req) }, + ) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/passwordless/api/emailExists.ts b/src/recipe/passwordless/api/emailExists.ts new file mode 100644 index 000000000..6ea3152de --- /dev/null +++ b/src/recipe/passwordless/api/emailExists.ts @@ -0,0 +1,41 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '../' + +export default async function emailExists(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.emailExistsGET === undefined) + return false + + const email = options.req.getKeyValueFromQuery('email') + + if (email === undefined || typeof email !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the email as a GET param', + }) + } + + const result = await apiImplementation.emailExistsGET({ + email, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/passwordless/api/implementation.ts b/src/recipe/passwordless/api/implementation.ts new file mode 100644 index 000000000..be8132e1f --- /dev/null +++ b/src/recipe/passwordless/api/implementation.ts @@ -0,0 +1,267 @@ +import { APIInterface } from '../' +import { logDebugMessage } from '../../../logger' +import EmailVerification from '../../emailverification/recipe' +import Session from '../../session' + +export default function getAPIImplementation(): APIInterface { + return { + async consumeCodePOST(input) { + const response = await input.options.recipeImplementation.consumeCode( + 'deviceId' in input + ? { + preAuthSessionId: input.preAuthSessionId, + deviceId: input.deviceId, + userInputCode: input.userInputCode, + userContext: input.userContext, + } + : { + preAuthSessionId: input.preAuthSessionId, + linkCode: input.linkCode, + userContext: input.userContext, + }, + ) + + if (response.status !== 'OK') + return response + + const user = response.user + + if (user.email !== undefined) { + const emailVerificationInstance = EmailVerification.getInstance() + if (emailVerificationInstance) { + const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( + { + userId: user.id, + email: user.email, + userContext: input.userContext, + }, + ) + + if (tokenResponse.status === 'OK') { + await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ + token: tokenResponse.token, + userContext: input.userContext, + }) + } + } + } + + const session = await Session.createNewSession( + input.options.req, + input.options.res, + user.id, + {}, + {}, + input.userContext, + ) + + return { + status: 'OK', + createdNewUser: response.createdNewUser, + user: response.user, + session, + } + }, + async createCodePOST(input) { + const response = await input.options.recipeImplementation.createCode( + 'email' in input + ? { + userContext: input.userContext, + email: input.email, + userInputCode: + input.options.config.getCustomUserInputCode === undefined + ? undefined + : await input.options.config.getCustomUserInputCode(input.userContext), + } + : { + userContext: input.userContext, + phoneNumber: input.phoneNumber, + userInputCode: + input.options.config.getCustomUserInputCode === undefined + ? undefined + : await input.options.config.getCustomUserInputCode(input.userContext), + }, + ) + + // now we send the email / text message. + let magicLink: string | undefined + let userInputCode: string | undefined + const flowType = input.options.config.flowType + if (flowType === 'MAGIC_LINK' || flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') { + magicLink + = `${input.options.appInfo.websiteDomain.getAsStringDangerous() + + input.options.appInfo.websiteBasePath.getAsStringDangerous() + }/verify` + + `?rid=${ + input.options.recipeId + }&preAuthSessionId=${ + response.preAuthSessionId + }#${ + response.linkCode}` + } + if (flowType === 'USER_INPUT_CODE' || flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + userInputCode = response.userInputCode + + // we don't do something special for serverless env here + // cause we want to wait for service's reply since it can show + // a UI error message for if sending an SMS / email failed or not. + if ( + input.options.config.contactMethod === 'PHONE' + || (input.options.config.contactMethod === 'EMAIL_OR_PHONE' && 'phoneNumber' in input) + ) { + logDebugMessage(`Sending passwordless login SMS to ${(input as any).phoneNumber}`) + await input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ + type: 'PASSWORDLESS_LOGIN', + codeLifetime: response.codeLifetime, + phoneNumber: (input as any).phoneNumber!, + preAuthSessionId: response.preAuthSessionId, + urlWithLinkCode: magicLink, + userInputCode, + userContext: input.userContext, + }) + } + else { + logDebugMessage(`Sending passwordless login email to ${(input as any).email}`) + await input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: 'PASSWORDLESS_LOGIN', + email: (input as any).email!, + codeLifetime: response.codeLifetime, + preAuthSessionId: response.preAuthSessionId, + urlWithLinkCode: magicLink, + userInputCode, + userContext: input.userContext, + }) + } + + return { + status: 'OK', + deviceId: response.deviceId, + flowType: input.options.config.flowType, + preAuthSessionId: response.preAuthSessionId, + } + }, + async emailExistsGET(input) { + const response = await input.options.recipeImplementation.getUserByEmail({ + userContext: input.userContext, + email: input.email, + }) + + return { + exists: response !== undefined, + status: 'OK', + } + }, + async phoneNumberExistsGET(input) { + const response = await input.options.recipeImplementation.getUserByPhoneNumber({ + userContext: input.userContext, + phoneNumber: input.phoneNumber, + }) + + return { + exists: response !== undefined, + status: 'OK', + } + }, + async resendCodePOST(input) { + const deviceInfo = await input.options.recipeImplementation.listCodesByDeviceId({ + userContext: input.userContext, + deviceId: input.deviceId, + }) + + if (deviceInfo === undefined) { + return { + status: 'RESTART_FLOW_ERROR', + } + } + + if ( + (input.options.config.contactMethod === 'PHONE' && deviceInfo.phoneNumber === undefined) + || (input.options.config.contactMethod === 'EMAIL' && deviceInfo.email === undefined) + ) { + return { + status: 'RESTART_FLOW_ERROR', + } + } + + let numberOfTriesToCreateNewCode = 0 + while (true) { + numberOfTriesToCreateNewCode++ + const response = await input.options.recipeImplementation.createNewCodeForDevice({ + userContext: input.userContext, + deviceId: input.deviceId, + userInputCode: + input.options.config.getCustomUserInputCode === undefined + ? undefined + : await input.options.config.getCustomUserInputCode(input.userContext), + }) + + if (response.status === 'USER_INPUT_CODE_ALREADY_USED_ERROR') { + if (numberOfTriesToCreateNewCode >= 3) { + // we retry 3 times. + return { + status: 'GENERAL_ERROR', + message: 'Failed to generate a one time code. Please try again', + } + } + continue + } + + if (response.status === 'OK') { + let magicLink: string | undefined + let userInputCode: string | undefined + const flowType = input.options.config.flowType + if (flowType === 'MAGIC_LINK' || flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') { + magicLink + = `${input.options.appInfo.websiteDomain.getAsStringDangerous() + + input.options.appInfo.websiteBasePath.getAsStringDangerous() + }/verify` + + `?rid=${ + input.options.recipeId + }&preAuthSessionId=${ + response.preAuthSessionId + }#${ + response.linkCode}` + } + if (flowType === 'USER_INPUT_CODE' || flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + userInputCode = response.userInputCode + + // we don't do something special for serverless env here + // cause we want to wait for service's reply since it can show + // a UI error message for if sending an SMS / email failed or not. + if ( + input.options.config.contactMethod === 'PHONE' + || (input.options.config.contactMethod === 'EMAIL_OR_PHONE' + && deviceInfo.phoneNumber !== undefined) + ) { + logDebugMessage(`Sending passwordless login SMS to ${(input as any).phoneNumber}`) + await input.options.smsDelivery.ingredientInterfaceImpl.sendSms({ + type: 'PASSWORDLESS_LOGIN', + codeLifetime: response.codeLifetime, + phoneNumber: deviceInfo.phoneNumber!, + preAuthSessionId: response.preAuthSessionId, + urlWithLinkCode: magicLink, + userInputCode, + userContext: input.userContext, + }) + } + else { + logDebugMessage(`Sending passwordless login email to ${(input as any).email}`) + await input.options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: 'PASSWORDLESS_LOGIN', + email: deviceInfo.email!, + codeLifetime: response.codeLifetime, + preAuthSessionId: response.preAuthSessionId, + urlWithLinkCode: magicLink, + userInputCode, + userContext: input.userContext, + }) + } + } + + return { + status: response.status, + } + } + }, + } +} diff --git a/src/recipe/passwordless/api/phoneNumberExists.ts b/src/recipe/passwordless/api/phoneNumberExists.ts new file mode 100644 index 000000000..e125cebd8 --- /dev/null +++ b/src/recipe/passwordless/api/phoneNumberExists.ts @@ -0,0 +1,44 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '..' + +export default async function phoneNumberExists( + apiImplementation: APIInterface, + options: APIOptions, +): Promise { + if (apiImplementation.phoneNumberExistsGET === undefined) + return false + + const phoneNumber = options.req.getKeyValueFromQuery('phoneNumber') + + if (phoneNumber === undefined || typeof phoneNumber !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the phoneNumber as a GET param', + }) + } + + const result = await apiImplementation.phoneNumberExistsGET({ + phoneNumber, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/passwordless/api/resendCode.ts b/src/recipe/passwordless/api/resendCode.ts new file mode 100644 index 000000000..2993dc589 --- /dev/null +++ b/src/recipe/passwordless/api/resendCode.ts @@ -0,0 +1,51 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '..' + +export default async function resendCode(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.resendCodePOST === undefined) + return false + + const body = await options.req.getJSONBody() + const preAuthSessionId = body.preAuthSessionId + const deviceId = body.deviceId + + if (preAuthSessionId === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide preAuthSessionId', + }) + } + + if (deviceId === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide a deviceId', + }) + } + + const result = await apiImplementation.resendCodePOST({ + deviceId, + preAuthSessionId, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/passwordless/constants.ts b/src/recipe/passwordless/constants.ts new file mode 100644 index 000000000..9bc8e1de1 --- /dev/null +++ b/src/recipe/passwordless/constants.ts @@ -0,0 +1,24 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const CREATE_CODE_API = '/signinup/code' + +export const RESEND_CODE_API = '/signinup/code/resend' + +export const CONSUME_CODE_API = '/signinup/code/consume' + +export const DOES_EMAIL_EXIST_API = '/signup/email/exists' + +export const DOES_PHONE_NUMBER_EXIST_API = '/signup/phonenumber/exists' diff --git a/src/recipe/passwordless/emaildelivery/index.ts b/src/recipe/passwordless/emaildelivery/index.ts new file mode 100644 index 000000000..ef147a362 --- /dev/null +++ b/src/recipe/passwordless/emaildelivery/index.ts @@ -0,0 +1 @@ +export * from './services' diff --git a/src/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts b/src/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts new file mode 100644 index 000000000..f5ce77549 --- /dev/null +++ b/src/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts @@ -0,0 +1,148 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios, { AxiosError } from 'axios' +import { TypePasswordlessEmailDeliveryInput } from '../../../types' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' +import { NormalisedAppinfo } from '../../../../../types' +import { logDebugMessage } from '../../../../../logger' + +function defaultCreateAndSendCustomEmail(appInfo: NormalisedAppinfo) { + return async ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + }, + _: any, + ): Promise => { + if (process.env.TEST_MODE === 'testing') + return + + try { + await axios({ + method: 'POST', + url: 'https://api.supertokens.io/0/st/auth/passwordless/login', + data: { + email: input.email, + appName: appInfo.appName, + codeLifetime: input.codeLifetime, + urlWithLinkCode: input.urlWithLinkCode, + userInputCode: input.userInputCode, + }, + headers: { + 'api-version': 0, + }, + }) + logDebugMessage(`Email sent to ${input.email}`) + } + catch (error) { + logDebugMessage('Error sending passwordless login email') + if (axios.isAxiosError(error)) { + const err = error as AxiosError + if (err.response) { + logDebugMessage(`Error status: ${err.response.status}`) + logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`) + } + else { + logDebugMessage(`Error: ${err.message}`) + } + } + else { + logDebugMessage(`Error: ${JSON.stringify(error)}`) + } + logDebugMessage('Logging the input below:') + logDebugMessage( + JSON.stringify( + { + email: input.email, + appName: appInfo.appName, + codeLifetime: input.codeLifetime, + urlWithLinkCode: input.urlWithLinkCode, + userInputCode: input.userInputCode, + }, + null, + 2, + ), + ) + /** + * if the error is thrown from API, the response object + * will be of type `{err: string}` + */ + if (axios.isAxiosError(error) && error.response !== undefined) { + if (error.response.data.err !== undefined) + throw new Error(error.response.data.err) + } + throw error + } + } +} + +export default class BackwardCompatibilityService +implements EmailDeliveryInterface { + private createAndSendCustomEmail: ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + + constructor( + appInfo: NormalisedAppinfo, + createAndSendCustomEmail?: ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise, + ) { + this.createAndSendCustomEmail + = createAndSendCustomEmail === undefined + ? defaultCreateAndSendCustomEmail(appInfo) + : createAndSendCustomEmail + } + + sendEmail = async (input: TypePasswordlessEmailDeliveryInput & { userContext: any }) => { + await this.createAndSendCustomEmail( + { + email: input.email, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + preAuthSessionId: input.preAuthSessionId, + codeLifetime: input.codeLifetime, + }, + input.userContext, + ) + } +} diff --git a/src/recipe/passwordless/emaildelivery/services/index.ts b/src/recipe/passwordless/emaildelivery/services/index.ts new file mode 100644 index 000000000..cdd3e3292 --- /dev/null +++ b/src/recipe/passwordless/emaildelivery/services/index.ts @@ -0,0 +1,16 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import SMTP from './smtp' +export const SMTPService = SMTP diff --git a/src/recipe/passwordless/emaildelivery/services/smtp/index.ts b/src/recipe/passwordless/emaildelivery/services/smtp/index.ts new file mode 100644 index 000000000..44c1120b7 --- /dev/null +++ b/src/recipe/passwordless/emaildelivery/services/smtp/index.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { createTransport } from 'nodemailer' +import OverrideableBuilder from 'overrideableBuilder' +import { ServiceInterface, TypeInput } from '../../../../../ingredients/emaildelivery/services/smtp' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' +import { TypePasswordlessEmailDeliveryInput } from '../../../types' +import { getServiceImplementation } from './serviceImplementation' + +export default class SMTPService implements EmailDeliveryInterface { + serviceImpl: ServiceInterface + + constructor(config: TypeInput) { + const transporter = createTransport({ + host: config.smtpSettings.host, + port: config.smtpSettings.port, + auth: { + user: config.smtpSettings.authUsername || config.smtpSettings.from.email, + pass: config.smtpSettings.password, + }, + secure: config.smtpSettings.secure, + }) + let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)) + if (config.override !== undefined) + builder = builder.override(config.override) + + this.serviceImpl = builder.build() + } + + sendEmail = async (input: TypePasswordlessEmailDeliveryInput & { userContext: any }) => { + const content = await this.serviceImpl.getContent(input) + await this.serviceImpl.sendRawEmail({ + ...content, + userContext: input.userContext, + }) + } +} diff --git a/src/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.ts b/src/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.ts new file mode 100644 index 000000000..b715e8222 --- /dev/null +++ b/src/recipe/passwordless/emaildelivery/services/smtp/passwordlessLogin.ts @@ -0,0 +1,2886 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypePasswordlessEmailDeliveryInput } from '../../../types' +import { GetContentResult } from '../../../../../ingredients/emaildelivery/services/smtp' +import Supertokens from '../../../../../supertokens' +import { humaniseMilliseconds } from '../../../../../utils' +export default function getPasswordlessLoginEmailContent(input: TypePasswordlessEmailDeliveryInput): GetContentResult { + const supertokens = Supertokens.getInstanceOrThrowError() + const appName = supertokens.appInfo.appName + const body = getPasswordlessLoginEmailHTML( + appName, + input.email, + input.codeLifetime, + input.urlWithLinkCode, + input.userInputCode, + ) + return { + body, + toEmail: input.email, + subject: 'Login to your account', + isHtml: true, + } +} + +function getPasswordlessLoginOTPBody(appName: string, email: string, codeLifetime: string, userInputCode: string) { + return ` + + + + + + + + *|MC:SUBJECT|* + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + + + + +
+

+ Login to ${appName}

+
+ + + + + + +
+ + +
+
+ +

+ Enter the below OTP in your login screen. Note + that the OTP expires in ${codeLifetime}.

+ +
+
+ ${userInputCode}
+ +
+
+
+
+ + + + + + +
+ + +

+ This email is meant for ${email} +

+
+
+ +
+ + + + + +
+ +
+ +
+
+ + + + ` +} + +function getPasswordlessLoginURLLinkBody( + appName: string, + email: string, + codeLifetime: string, + urlWithLinkCode: string, +) { + return ` + + + + + + + + *|MC:SUBJECT|* + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + + + + +
+

+ Login to ${appName}

+
+ + + + + + +
+ + +
+
+ +

+ Please click the button below to sign in / up. + Note that the link expires in ${codeLifetime}. +

+ +
+ Login +
+
+
+

+ Alternatively, you can directly paste this link + in your browser
+ ${urlWithLinkCode} +

+
+
+ + + + +
+ + + + + + +
+ + +

+ This email is meant for ${email} +

+
+
+ +
+ + + + + +
+ +
+ +
+
+ + + + ` +} + +function getPasswordlessLoginOTPAndURLLinkBody( + appName: string, + email: string, + codeLifetime: string, + urlWithLinkCode: string, + userInputCode: string, +) { + return ` + + + + + + + + *|MC:SUBJECT|* + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + + + + +
+

+ Login to ${appName}

+
+ + + + + + +
+
+
+

+ Enter the below OTP in your login screen. Note + that the OTP expires in ${codeLifetime}.

+
+ +
+
+ ${userInputCode}
+
+
+
+ + + + + + +
+ + + + + + + + + + +
+ + or +
+ + + +
+ + + + + + +
+ + +
+
+ +

+ Please click the button below to sign in / up. + Note that the link expires in ${codeLifetime}.

+ +
+ Login +
+
+ +
+

+ Alternatively, you can directly paste this link + in your browser
+ ${urlWithLinkCode} +

+
+
+ + +
+ + + + + + +
+

+ This email is meant for ${email} +

+
+
+ +
+ + + + + +
+ +
+ +
+
+ + + + ` +} + +export function getPasswordlessLoginEmailHTML( + appName: string, + email: string, + codeLifetime: number, + urlWithLinkCode?: string, + userInputCode?: string, +): string { + if (urlWithLinkCode !== undefined && userInputCode !== undefined) { + return getPasswordlessLoginOTPAndURLLinkBody( + appName, + email, + humaniseMilliseconds(codeLifetime), + urlWithLinkCode, + userInputCode, + ) + } + if (userInputCode !== undefined) + return getPasswordlessLoginOTPBody(appName, email, humaniseMilliseconds(codeLifetime), userInputCode) + + if (urlWithLinkCode !== undefined) + return getPasswordlessLoginURLLinkBody(appName, email, humaniseMilliseconds(codeLifetime), urlWithLinkCode) + + throw new Error('this should never be thrown') +} diff --git a/src/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts b/src/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts new file mode 100644 index 000000000..a551fc90a --- /dev/null +++ b/src/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts @@ -0,0 +1,57 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { Transporter } from 'nodemailer' +import { TypePasswordlessEmailDeliveryInput } from '../../../types' +import { + GetContentResult, + ServiceInterface, + TypeInputSendRawEmail, +} from '../../../../../ingredients/emaildelivery/services/smtp' +import getPasswordlessLoginEmailContent from './passwordlessLogin' + +export function getServiceImplementation( + transporter: Transporter, + from: { + name: string + email: string + }, +): ServiceInterface { + return { + async sendRawEmail(input: TypeInputSendRawEmail) { + if (input.isHtml) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, + }) + } + else { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + text: input.body, + }) + } + }, + async getContent( + input: TypePasswordlessEmailDeliveryInput & { userContext: any }, + ): Promise { + return getPasswordlessLoginEmailContent(input) + }, + } +} diff --git a/src/recipe/passwordless/error.ts b/src/recipe/passwordless/error.ts new file mode 100644 index 000000000..cf8e10d10 --- /dev/null +++ b/src/recipe/passwordless/error.ts @@ -0,0 +1,25 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../../error' + +export default class SessionError extends STError { + constructor(options: { type: 'BAD_INPUT_ERROR'; message: string }) { + super({ + ...options, + }) + this.fromRecipe = 'passwordless' + } +} diff --git a/src/recipe/passwordless/index.ts b/src/recipe/passwordless/index.ts new file mode 100644 index 000000000..668faebc9 --- /dev/null +++ b/src/recipe/passwordless/index.ts @@ -0,0 +1,214 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import Recipe from './recipe' +import SuperTokensError from './error' +import { + APIInterface, + APIOptions, + RecipeInterface, + TypePasswordlessEmailDeliveryInput, + TypePasswordlessSmsDeliveryInput, + User, +} from './types' + +export default class Wrapper { + static init = Recipe.init + + static Error = SuperTokensError + + static createCode( + input: ( + | { + email: string + } + | { + phoneNumber: string + } + ) & { userInputCode?: string; userContext?: any }, + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ + userContext: {}, + ...input, + }) + } + + static createNewCodeForDevice(input: { deviceId: string; userInputCode?: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ + userContext: {}, + ...input, + }) + } + + static consumeCode( + input: + | { + preAuthSessionId: string + userInputCode: string + deviceId: string + userContext?: any + } + | { + preAuthSessionId: string + linkCode: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ userContext: {}, ...input }) + } + + static getUserById(input: { userId: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userContext: {}, ...input }) + } + + static getUserByEmail(input: { email: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ userContext: {}, ...input }) + } + + static getUserByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByPhoneNumber({ userContext: {}, ...input }) + } + + static updateUser(input: { + userId: string + email?: string | null + phoneNumber?: string | null + userContext?: any + }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateUser({ userContext: {}, ...input }) + } + + static revokeAllCodes( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ userContext: {}, ...input }) + } + + static revokeCode(input: { codeId: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ userContext: {}, ...input }) + } + + static listCodesByEmail(input: { email: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ userContext: {}, ...input }) + } + + static listCodesByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ + userContext: {}, + ...input, + }) + } + + static listCodesByDeviceId(input: { deviceId: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ userContext: {}, ...input }) + } + + static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ + userContext: {}, + ...input, + }) + } + + static createMagicLink( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().createMagicLink({ userContext: {}, ...input }) + } + + static signInUp( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().signInUp({ userContext: {}, ...input }) + } + + static async sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: any }) { + return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ + userContext: {}, + ...input, + }) + } + + static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { + return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ + userContext: {}, + ...input, + }) + } +} + +export const init = Wrapper.init + +export const Error = Wrapper.Error + +export const createCode = Wrapper.createCode + +export const consumeCode = Wrapper.consumeCode + +export const getUserByEmail = Wrapper.getUserByEmail + +export const getUserById = Wrapper.getUserById + +export const getUserByPhoneNumber = Wrapper.getUserByPhoneNumber + +export const listCodesByDeviceId = Wrapper.listCodesByDeviceId + +export const listCodesByEmail = Wrapper.listCodesByEmail + +export const listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber + +export const listCodesByPreAuthSessionId = Wrapper.listCodesByPreAuthSessionId + +export const createNewCodeForDevice = Wrapper.createNewCodeForDevice + +export const updateUser = Wrapper.updateUser + +export const revokeAllCodes = Wrapper.revokeAllCodes + +export const revokeCode = Wrapper.revokeCode + +export const createMagicLink = Wrapper.createMagicLink + +export const signInUp = Wrapper.signInUp + +export type { RecipeInterface, User, APIOptions, APIInterface } + +export const sendEmail = Wrapper.sendEmail + +export const sendSms = Wrapper.sendSms diff --git a/src/recipe/passwordless/recipe.ts b/src/recipe/passwordless/recipe.ts new file mode 100644 index 000000000..7a5f540a4 --- /dev/null +++ b/src/recipe/passwordless/recipe.ts @@ -0,0 +1,330 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import NormalisedURLPath from '../../normalisedURLPath' +import EmailVerificationRecipe from '../emailverification/recipe' +import { Querier } from '../../querier' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import SmsDeliveryIngredient from '../../ingredients/smsdelivery' +import { GetEmailForUserIdFunc } from '../emailverification/types' +import { PostSuperTokensInitCallbacks } from '../../postSuperTokensInitCallbacks' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput, TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput } from './types' +import STError from './error' +import { validateAndNormaliseUserInput } from './utils' +import RecipeImplementation from './recipeImplementation' +import APIImplementation from './api/implementation' +import consumeCodeAPI from './api/consumeCode' +import createCodeAPI from './api/createCode' +import emailExistsAPI from './api/emailExists' +import phoneNumberExistsAPI from './api/phoneNumberExists' +import resendCodeAPI from './api/resendCode' +import { + CONSUME_CODE_API, + CREATE_CODE_API, + DOES_EMAIL_EXIST_API, + DOES_PHONE_NUMBER_EXIST_API, + RESEND_CODE_API, +} from './constants' + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined + static RECIPE_ID = 'passwordless' + + config: TypeNormalisedInput + + recipeInterfaceImpl: RecipeInterface + + apiImpl: APIInterface + + isInServerlessEnv: boolean + + emailDelivery: EmailDeliveryIngredient + + smsDelivery: SmsDeliveryIngredient + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + ingredients: { + emailDelivery: EmailDeliveryIngredient | undefined + smsDelivery: SmsDeliveryIngredient | undefined + }, + ) { + super(recipeId, appInfo) + this.isInServerlessEnv = isInServerlessEnv + this.config = validateAndNormaliseUserInput(this, appInfo, config) + + { + const builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + + /** + * emailDelivery will always needs to be declared after isInServerlessEnv + * and recipeInterfaceImpl values are set + */ + this.emailDelivery + = ingredients.emailDelivery === undefined + ? new EmailDeliveryIngredient(this.config.getEmailDeliveryConfig()) + : ingredients.emailDelivery + + this.smsDelivery + = ingredients.smsDelivery === undefined + ? new SmsDeliveryIngredient(this.config.getSmsDeliveryConfig()) + : ingredients.smsDelivery + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const emailVerificationRecipe = EmailVerificationRecipe.getInstance() + if (emailVerificationRecipe !== undefined) + emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)) + }) + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static init(config: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { + emailDelivery: undefined, + smsDelivery: undefined, + }) + return Recipe.instance + } + else { + throw new Error('Passwordless recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + // abstract instance functions below............... + + getAPIsHandled = (): APIHandled[] => { + return [ + { + id: CONSUME_CODE_API, + disabled: this.apiImpl.consumeCodePOST === undefined, + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(CONSUME_CODE_API), + }, + { + id: CREATE_CODE_API, + disabled: this.apiImpl.createCodePOST === undefined, + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(CREATE_CODE_API), + }, + { + id: DOES_EMAIL_EXIST_API, + disabled: this.apiImpl.emailExistsGET === undefined, + method: 'get', + pathWithoutApiBasePath: new NormalisedURLPath(DOES_EMAIL_EXIST_API), + }, + { + id: DOES_PHONE_NUMBER_EXIST_API, + disabled: this.apiImpl.phoneNumberExistsGET === undefined, + method: 'get', + pathWithoutApiBasePath: new NormalisedURLPath(DOES_PHONE_NUMBER_EXIST_API), + }, + { + id: RESEND_CODE_API, + disabled: this.apiImpl.resendCodePOST === undefined, + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(RESEND_CODE_API), + }, + ] + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + _: NormalisedURLPath, + __: HTTPMethod, + ): Promise => { + const options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + emailDelivery: this.emailDelivery, + smsDelivery: this.smsDelivery, + appInfo: this.getAppInfo(), + } + if (id === CONSUME_CODE_API) + return await consumeCodeAPI(this.apiImpl, options) + else if (id === CREATE_CODE_API) + return await createCodeAPI(this.apiImpl, options) + else if (id === DOES_EMAIL_EXIST_API) + return await emailExistsAPI(this.apiImpl, options) + else if (id === DOES_PHONE_NUMBER_EXIST_API) + return await phoneNumberExistsAPI(this.apiImpl, options) + else + return await resendCodeAPI(this.apiImpl, options) + } + + handleError = async (err: STError, _: BaseRequest, __: BaseResponse): Promise => { + throw err + } + + getAllCORSHeaders = (): string[] => { + return [] + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } + + // helper functions below... + + createMagicLink = async ( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ): Promise => { + const userInputCode + = this.config.getCustomUserInputCode !== undefined + ? await this.config.getCustomUserInputCode(input.userContext) + : undefined + + const codeInfo = await this.recipeInterfaceImpl.createCode( + 'email' in input + ? { + email: input.email, + userInputCode, + userContext: input.userContext, + } + : { + phoneNumber: input.phoneNumber, + userInputCode, + userContext: input.userContext, + }, + ) + + const appInfo = this.getAppInfo() + + const magicLink + = `${appInfo.websiteDomain.getAsStringDangerous() + + appInfo.websiteBasePath.getAsStringDangerous() + }/verify` + + `?rid=${ + this.getRecipeId() + }&preAuthSessionId=${ + codeInfo.preAuthSessionId + }#${ + codeInfo.linkCode}` + + return magicLink + } + + signInUp = async ( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ) => { + const codeInfo = await this.recipeInterfaceImpl.createCode( + 'email' in input + ? { + email: input.email, + userContext: input.userContext, + } + : { + phoneNumber: input.phoneNumber, + userContext: input.userContext, + }, + ) + + const consumeCodeResponse = await this.recipeInterfaceImpl.consumeCode( + this.config.flowType === 'MAGIC_LINK' + ? { + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + userContext: input.userContext, + } + : { + preAuthSessionId: codeInfo.preAuthSessionId, + deviceId: codeInfo.deviceId, + userInputCode: codeInfo.userInputCode, + userContext: input.userContext, + }, + ) + + if (consumeCodeResponse.status === 'OK') { + return { + status: 'OK', + createdNewUser: consumeCodeResponse.createdNewUser, + user: consumeCodeResponse.user, + } + } + else { + throw new Error('Failed to create user. Please retry') + } + } + + // helper functions... + getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { + const userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }) + if (userInfo !== undefined) { + if (userInfo.email !== undefined) { + return { + status: 'OK', + email: userInfo.email, + } + } + return { + status: 'EMAIL_DOES_NOT_EXIST_ERROR', + } + } + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } +} diff --git a/src/recipe/passwordless/recipeImplementation.ts b/src/recipe/passwordless/recipeImplementation.ts new file mode 100644 index 000000000..9b3999706 --- /dev/null +++ b/src/recipe/passwordless/recipeImplementation.ts @@ -0,0 +1,118 @@ +import { Querier } from '../../querier' +import NormalisedURLPath from '../../normalisedURLPath' +import { RecipeInterface } from './types' + +export default function getRecipeInterface(querier: Querier): RecipeInterface { + function copyAndRemoveUserContext(input: any): any { + const result = { + ...input, + } + delete result.userContext + return result + } + + return { + async consumeCode(input) { + const response = await querier.sendPostRequest( + new NormalisedURLPath('/recipe/signinup/code/consume'), + copyAndRemoveUserContext(input), + ) + return response + }, + async createCode(input) { + const response = await querier.sendPostRequest( + new NormalisedURLPath('/recipe/signinup/code'), + copyAndRemoveUserContext(input), + ) + return response + }, + async createNewCodeForDevice(input) { + const response = await querier.sendPostRequest( + new NormalisedURLPath('/recipe/signinup/code'), + copyAndRemoveUserContext(input), + ) + return response + }, + async getUserByEmail(input) { + const response = await querier.sendGetRequest( + new NormalisedURLPath('/recipe/user'), + copyAndRemoveUserContext(input), + ) + if (response.status === 'OK') + return response.user + + return undefined + }, + async getUserById(input) { + const response = await querier.sendGetRequest( + new NormalisedURLPath('/recipe/user'), + copyAndRemoveUserContext(input), + ) + if (response.status === 'OK') + return response.user + + return undefined + }, + async getUserByPhoneNumber(input) { + const response = await querier.sendGetRequest( + new NormalisedURLPath('/recipe/user'), + copyAndRemoveUserContext(input), + ) + if (response.status === 'OK') + return response.user + + return undefined + }, + async listCodesByDeviceId(input) { + const response = await querier.sendGetRequest( + new NormalisedURLPath('/recipe/signinup/codes'), + copyAndRemoveUserContext(input), + ) + return response.devices.length === 1 ? response.devices[0] : undefined + }, + async listCodesByEmail(input) { + const response = await querier.sendGetRequest( + new NormalisedURLPath('/recipe/signinup/codes'), + copyAndRemoveUserContext(input), + ) + return response.devices + }, + async listCodesByPhoneNumber(input) { + const response = await querier.sendGetRequest( + new NormalisedURLPath('/recipe/signinup/codes'), + copyAndRemoveUserContext(input), + ) + return response.devices + }, + async listCodesByPreAuthSessionId(input) { + const response = await querier.sendGetRequest( + new NormalisedURLPath('/recipe/signinup/codes'), + copyAndRemoveUserContext(input), + ) + return response.devices.length === 1 ? response.devices[0] : undefined + }, + async revokeAllCodes(input) { + await querier.sendPostRequest( + new NormalisedURLPath('/recipe/signinup/codes/remove'), + copyAndRemoveUserContext(input), + ) + return { + status: 'OK', + } + }, + async revokeCode(input) { + await querier.sendPostRequest( + new NormalisedURLPath('/recipe/signinup/code/remove'), + copyAndRemoveUserContext(input), + ) + return { status: 'OK' } + }, + async updateUser(input) { + const response = await querier.sendPutRequest( + new NormalisedURLPath('/recipe/user'), + copyAndRemoveUserContext(input), + ) + return response + }, + } +} diff --git a/src/recipe/passwordless/smsdelivery/index.ts b/src/recipe/passwordless/smsdelivery/index.ts new file mode 100644 index 000000000..ef147a362 --- /dev/null +++ b/src/recipe/passwordless/smsdelivery/index.ts @@ -0,0 +1 @@ +export * from './services' diff --git a/src/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts b/src/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts new file mode 100644 index 000000000..10c46bff6 --- /dev/null +++ b/src/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts @@ -0,0 +1,172 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios, { AxiosError } from 'axios' +import { TypePasswordlessSmsDeliveryInput } from '../../../types' +import { SmsDeliveryInterface } from '../../../../../ingredients/smsdelivery/types' +import { NormalisedAppinfo } from '../../../../../types' +import { SUPERTOKENS_SMS_SERVICE_URL } from '../../../../../ingredients/smsdelivery/services/supertokens' +import Supertokens from '../../../../../supertokens' +import { logDebugMessage } from '../../../../../logger' + +function defaultCreateAndSendCustomSms(_: NormalisedAppinfo) { + return async ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + }, + _: any, + ): Promise => { + const supertokens = Supertokens.getInstanceOrThrowError() + const appName = supertokens.appInfo.appName + try { + await axios({ + method: 'post', + url: SUPERTOKENS_SMS_SERVICE_URL, + data: { + smsInput: { + appName, + type: 'PASSWORDLESS_LOGIN', + phoneNumber: input.phoneNumber, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + codeLifetime: input.codeLifetime, + }, + }, + headers: { + 'api-version': '0', + }, + }) + logDebugMessage(`Passwordless login SMS sent to ${input.phoneNumber}`) + return + } + catch (error) { + logDebugMessage('Error sending passwordless login SMS') + if (axios.isAxiosError(error)) { + const err = error as AxiosError + if (err.response) { + logDebugMessage(`Error status: ${err.response.status}`) + logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`) + } + else { + logDebugMessage(`Error: ${err.message}`) + } + if (err.response) { + if (err.response.status !== 429) { + /** + * if the error is thrown from API, the response object + * will be of type `{err: string}` + */ + if (err.response.data.err !== undefined) + throw new Error(err.response.data.err) + else + throw err + } + } + else { + throw err + } + } + else { + logDebugMessage(`Error: ${JSON.stringify(error)}`) + throw error + } + } + console.log( + 'Free daily SMS quota reached. If you want to use SuperTokens to send SMS, please sign up on supertokens.com to get your SMS API key, else you can also define your own method by overriding the service. For now, we are logging it below:', + ) + /** + * if we do console.log(`SMS content: ${input}`); + * Output would be: + * SMS content: [object Object] + */ + /** + * JSON.stringify takes 3 inputs + * - value: usually an object or array, to be converted + * - replacer: An array of strings and numbers that acts + * as an approved list for selecting the object + * properties that will be stringified + * - space: Adds indentation, white space, and line break characters + * to the return-value JSON text to make it easier to read + * + * console.log(JSON.stringify({"a": 1, "b": 2})) + * Output: + * {"a":1,"b":2} + * + * console.log(JSON.stringify({"a": 1, "b": 2}, null, 2)) + * Output: + * { + * "a": 1, + * "b": 2 + * } + */ + console.log(`\nSMS content: ${JSON.stringify(input, null, 2)}`) + } +} + +export default class BackwardCompatibilityService implements SmsDeliveryInterface { + private createAndSendCustomSms: ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + + constructor( + appInfo: NormalisedAppinfo, + createAndSendCustomSms?: ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise, + ) { + this.createAndSendCustomSms + = createAndSendCustomSms === undefined ? defaultCreateAndSendCustomSms(appInfo) : createAndSendCustomSms + } + + sendSms = async (input: TypePasswordlessSmsDeliveryInput & { userContext: any }) => { + await this.createAndSendCustomSms( + { + phoneNumber: input.phoneNumber, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + preAuthSessionId: input.preAuthSessionId, + codeLifetime: input.codeLifetime, + }, + input.userContext, + ) + } +} diff --git a/src/recipe/passwordless/smsdelivery/services/index.ts b/src/recipe/passwordless/smsdelivery/services/index.ts new file mode 100644 index 000000000..f7e1ab546 --- /dev/null +++ b/src/recipe/passwordless/smsdelivery/services/index.ts @@ -0,0 +1,19 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import Twilio from './twilio' +import Supertokens from './supertokens' + +export const TwilioService = Twilio +export const SupertokensService = Supertokens diff --git a/src/recipe/passwordless/smsdelivery/services/supertokens/index.ts b/src/recipe/passwordless/smsdelivery/services/supertokens/index.ts new file mode 100644 index 000000000..6939cd8c9 --- /dev/null +++ b/src/recipe/passwordless/smsdelivery/services/supertokens/index.ts @@ -0,0 +1,85 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios, { AxiosError } from 'axios' +import { SUPERTOKENS_SMS_SERVICE_URL } from '../../../../../ingredients/smsdelivery/services/supertokens' +import { SmsDeliveryInterface } from '../../../../../ingredients/smsdelivery/types' +import { TypePasswordlessSmsDeliveryInput } from '../../../types' +import Supertokens from '../../../../../supertokens' +import { logDebugMessage } from '../../../../../logger' + +export default class SupertokensService implements SmsDeliveryInterface { + private apiKey: string + + constructor(apiKey: string) { + this.apiKey = apiKey + } + + sendSms = async (input: TypePasswordlessSmsDeliveryInput) => { + const supertokens = Supertokens.getInstanceOrThrowError() + const appName = supertokens.appInfo.appName + try { + await axios({ + method: 'post', + url: SUPERTOKENS_SMS_SERVICE_URL, + data: { + apiKey: this.apiKey, + smsInput: { + type: input.type, + phoneNumber: input.phoneNumber, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + codeLifetime: input.codeLifetime, + appName, + }, + }, + headers: { + 'api-version': '0', + }, + }) + } + catch (error) { + logDebugMessage('Error sending SMS') + if (axios.isAxiosError(error)) { + const err = error as AxiosError + if (err.response) { + logDebugMessage(`Error status: ${err.response.status}`) + logDebugMessage(`Error response: ${JSON.stringify(err.response.data)}`) + } + else { + logDebugMessage(`Error: ${err.message}`) + } + } + else { + logDebugMessage(`Error: ${JSON.stringify(error)}`) + } + logDebugMessage('Logging the input below:') + logDebugMessage( + JSON.stringify( + { + type: input.type, + phoneNumber: input.phoneNumber, + userInputCode: input.userInputCode, + urlWithLinkCode: input.urlWithLinkCode, + codeLifetime: input.codeLifetime, + appName, + }, + null, + 2, + ), + ) + throw error + } + } +} diff --git a/src/recipe/passwordless/smsdelivery/services/twilio/index.ts b/src/recipe/passwordless/smsdelivery/services/twilio/index.ts new file mode 100644 index 000000000..bd043311e --- /dev/null +++ b/src/recipe/passwordless/smsdelivery/services/twilio/index.ts @@ -0,0 +1,61 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import Twilio from 'twilio' +import OverrideableBuilder from 'overrideableBuilder' +import { + ServiceInterface, + TypeInput, + normaliseUserInputConfig, +} from '../../../../../ingredients/smsdelivery/services/twilio' +import { SmsDeliveryInterface } from '../../../../../ingredients/smsdelivery/types' +import { TypePasswordlessSmsDeliveryInput } from '../../../types' +import { getServiceImplementation } from './serviceImplementation' + +export default class TwilioService implements SmsDeliveryInterface { + serviceImpl: ServiceInterface + private config: TypeInput + + constructor(config: TypeInput) { + this.config = normaliseUserInputConfig(config) + const twilioClient = Twilio( + config.twilioSettings.accountSid, + config.twilioSettings.authToken, + config.twilioSettings.opts, + ) + let builder = new OverrideableBuilder(getServiceImplementation(twilioClient)) + if (config.override !== undefined) + builder = builder.override(config.override) + + this.serviceImpl = builder.build() + } + + sendSms = async (input: TypePasswordlessSmsDeliveryInput & { userContext: any }) => { + const content = await this.serviceImpl.getContent(input) + if ('from' in this.config.twilioSettings) { + await this.serviceImpl.sendRawSms({ + ...content, + userContext: input.userContext, + from: this.config.twilioSettings.from, + }) + } + else { + await this.serviceImpl.sendRawSms({ + ...content, + userContext: input.userContext, + messagingServiceSid: this.config.twilioSettings.messagingServiceSid, + }) + } + } +} diff --git a/src/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.ts b/src/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.ts new file mode 100644 index 000000000..2164e3664 --- /dev/null +++ b/src/recipe/passwordless/smsdelivery/services/twilio/passwordlessLogin.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypePasswordlessSmsDeliveryInput } from '../../../types' +import { GetContentResult } from '../../../../../ingredients/smsdelivery/services/twilio' +import { humaniseMilliseconds } from '../../../../../utils' +import Supertokens from '../../../../../supertokens' + +export default function getPasswordlessLoginSmsContent(input: TypePasswordlessSmsDeliveryInput): GetContentResult { + const supertokens = Supertokens.getInstanceOrThrowError() + const appName = supertokens.appInfo.appName + const body = getPasswordlessLoginSmsBody(appName, input.codeLifetime, input.urlWithLinkCode, input.userInputCode) + return { + body, + toPhoneNumber: input.phoneNumber, + } +} + +function getPasswordlessLoginSmsBody( + appName: string, + codeLifetime: number, + urlWithLinkCode: string | undefined, + userInputCode: string | undefined, +) { + let message = '' + if (urlWithLinkCode !== undefined && userInputCode !== undefined) + message += `OTP to login is ${userInputCode} for ${appName}\n\nOR click ${urlWithLinkCode} to login.\n\n` + else if (urlWithLinkCode !== undefined) + message += `Click ${urlWithLinkCode} to login to ${appName}\n\n` + else + message += `OTP to login is ${userInputCode} for ${appName}\n\n` + + const humanisedCodeLifetime = humaniseMilliseconds(codeLifetime) + message += `This is valid for ${humanisedCodeLifetime}.` + return message +} diff --git a/src/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts b/src/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts new file mode 100644 index 000000000..df48a4161 --- /dev/null +++ b/src/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import Twilio from 'twilio/lib/rest/Twilio' +import { TypePasswordlessSmsDeliveryInput } from '../../../types' +import { + GetContentResult, + ServiceInterface, + TypeInputSendRawSms, +} from '../../../../../ingredients/smsdelivery/services/twilio' +import getPasswordlessLoginSmsContent from './passwordlessLogin' + +export function getServiceImplementation(twilioClient: Twilio): ServiceInterface { + return { + async sendRawSms(input: TypeInputSendRawSms) { + if ('from' in input) { + await twilioClient.messages.create({ + to: input.toPhoneNumber, + body: input.body, + from: input.from, + }) + } + else { + await twilioClient.messages.create({ + to: input.toPhoneNumber, + body: input.body, + messagingServiceSid: input.messagingServiceSid, + }) + } + }, + async getContent( + input: TypePasswordlessSmsDeliveryInput & { userContext: any }, + ): Promise { + return getPasswordlessLoginSmsContent(input) + }, + } +} diff --git a/src/recipe/passwordless/types.ts b/src/recipe/passwordless/types.ts new file mode 100644 index 000000000..bd3fbc356 --- /dev/null +++ b/src/recipe/passwordless/types.ts @@ -0,0 +1,415 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { SessionContainerInterface } from '../session/types' +import { + TypeInput as EmailDeliveryTypeInput, + TypeInputWithService as EmailDeliveryTypeInputWithService, +} from '../../ingredients/emaildelivery/types' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import { + TypeInput as SmsDeliveryTypeInput, + TypeInputWithService as SmsDeliveryTypeInputWithService, +} from '../../ingredients/smsdelivery/types' +import SmsDeliveryIngredient from '../../ingredients/smsdelivery' +import { GeneralErrorResponse, NormalisedAppinfo } from '../../types' + +// As per https://github.com/supertokens/supertokens-core/issues/325 + +export interface User { + id: string + email?: string + phoneNumber?: string + timeJoined: number +} + +export type TypeInput = ( + | { + contactMethod: 'PHONE' + validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use smsDelivery config instead + */ + createAndSendCustomTextMessage?: ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + } + | { + contactMethod: 'EMAIL' + validateEmailAddress?: (email: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use emailDelivery config instead + */ + createAndSendCustomEmail?: ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + } + | { + contactMethod: 'EMAIL_OR_PHONE' + validateEmailAddress?: (email: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use emailDelivery config instead + */ + createAndSendCustomEmail?: ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use smsDelivery config instead + */ + createAndSendCustomTextMessage?: ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + } +) & { + flowType: 'USER_INPUT_CODE' | 'MAGIC_LINK' | 'USER_INPUT_CODE_AND_MAGIC_LINK' + + emailDelivery?: EmailDeliveryTypeInput + smsDelivery?: SmsDeliveryTypeInput + + // Override this to override how user input codes are generated + // By default (=undefined) it is done in the Core + getCustomUserInputCode?: (userContext: any) => Promise | string + + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder: OverrideableBuilder) => APIInterface + } +} + +export type TypeNormalisedInput = ( + | { + contactMethod: 'PHONE' + validatePhoneNumber: (phoneNumber: string) => Promise | string | undefined + } + | { + contactMethod: 'EMAIL' + validateEmailAddress: (email: string) => Promise | string | undefined + } + | { + contactMethod: 'EMAIL_OR_PHONE' + validateEmailAddress: (email: string) => Promise | string | undefined + + validatePhoneNumber: (phoneNumber: string) => Promise | string | undefined + } +) & { + flowType: 'USER_INPUT_CODE' | 'MAGIC_LINK' | 'USER_INPUT_CODE_AND_MAGIC_LINK' + + // Override this to override how user input codes are generated + // By default (=undefined) it is done in the Core + getCustomUserInputCode?: (userContext: any) => Promise | string + + getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService + getEmailDeliveryConfig: () => EmailDeliveryTypeInputWithService + override: { + functions: ( + originalImplementation: RecipeInterface, + builder: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder: OverrideableBuilder) => APIInterface + } +} + +export interface RecipeInterface { + createCode: ( + input: ( + | { + email: string + } + | { + phoneNumber: string + } + ) & { userInputCode?: string; userContext: any } + ) => Promise<{ + status: 'OK' + preAuthSessionId: string + codeId: string + deviceId: string + userInputCode: string + linkCode: string + codeLifetime: number + timeCreated: number + }> + createNewCodeForDevice: (input: { + deviceId: string + userInputCode?: string + userContext: any + }) => Promise< + | { + status: 'OK' + preAuthSessionId: string + codeId: string + deviceId: string + userInputCode: string + linkCode: string + codeLifetime: number + timeCreated: number + } + | { status: 'RESTART_FLOW_ERROR' | 'USER_INPUT_CODE_ALREADY_USED_ERROR' } + > + consumeCode: ( + input: + | { + userInputCode: string + deviceId: string + preAuthSessionId: string + userContext: any + } + | { + linkCode: string + preAuthSessionId: string + userContext: any + } + ) => Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + } + | { + status: 'INCORRECT_USER_INPUT_CODE_ERROR' | 'EXPIRED_USER_INPUT_CODE_ERROR' + failedCodeInputAttemptCount: number + maximumCodeInputAttempts: number + } + | { status: 'RESTART_FLOW_ERROR' } + > + + getUserById: (input: { userId: string; userContext: any }) => Promise + getUserByEmail: (input: { email: string; userContext: any }) => Promise + getUserByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise + + updateUser: (input: { + userId: string + email?: string | null + phoneNumber?: string | null + userContext: any + }) => Promise<{ + status: 'OK' | 'UNKNOWN_USER_ID_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' | 'PHONE_NUMBER_ALREADY_EXISTS_ERROR' + }> + + revokeAllCodes: ( + input: + | { + email: string + userContext: any + } + | { + phoneNumber: string + userContext: any + } + ) => Promise<{ + status: 'OK' + }> + + revokeCode: (input: { + codeId: string + userContext: any + }) => Promise<{ + status: 'OK' + }> + + listCodesByEmail: (input: { email: string; userContext: any }) => Promise + + listCodesByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise + + listCodesByDeviceId: (input: { deviceId: string; userContext: any }) => Promise + + listCodesByPreAuthSessionId: (input: { + preAuthSessionId: string + userContext: any + }) => Promise +} + +export interface DeviceType { + preAuthSessionId: string + + failedCodeInputAttemptCount: number + + email?: string + phoneNumber?: string + + codes: { + codeId: string + timeCreated: string + codeLifetime: number + }[] +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + appInfo: NormalisedAppinfo + config: TypeNormalisedInput + recipeId: string + isInServerlessEnv: boolean + req: BaseRequest + res: BaseResponse + emailDelivery: EmailDeliveryIngredient + smsDelivery: SmsDeliveryIngredient +} + +export interface APIInterface { + createCodePOST?: ( + input: ({ email: string } | { phoneNumber: string }) & { + options: APIOptions + userContext: any + } + ) => Promise< + | { + status: 'OK' + deviceId: string + preAuthSessionId: string + flowType: 'USER_INPUT_CODE' | 'MAGIC_LINK' | 'USER_INPUT_CODE_AND_MAGIC_LINK' + } + | GeneralErrorResponse + > + + resendCodePOST?: ( + input: { deviceId: string; preAuthSessionId: string } & { + options: APIOptions + userContext: any + } + ) => Promise + + consumeCodePOST?: ( + input: ( + | { + userInputCode: string + deviceId: string + preAuthSessionId: string + } + | { + linkCode: string + preAuthSessionId: string + } + ) & { + options: APIOptions + userContext: any + } + ) => Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + session: SessionContainerInterface + } + | { + status: 'INCORRECT_USER_INPUT_CODE_ERROR' | 'EXPIRED_USER_INPUT_CODE_ERROR' + failedCodeInputAttemptCount: number + maximumCodeInputAttempts: number + } + | GeneralErrorResponse + | { status: 'RESTART_FLOW_ERROR' } + > + + emailExistsGET?: (input: { + email: string + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + exists: boolean + } + | GeneralErrorResponse + > + + phoneNumberExistsGET?: (input: { + phoneNumber: string + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + exists: boolean + } + | GeneralErrorResponse + > +} + +export interface TypePasswordlessEmailDeliveryInput { + type: 'PASSWORDLESS_LOGIN' + email: string + userInputCode?: string + urlWithLinkCode?: string + codeLifetime: number + preAuthSessionId: string +} + +export interface TypePasswordlessSmsDeliveryInput { + type: 'PASSWORDLESS_LOGIN' + phoneNumber: string + userInputCode?: string + urlWithLinkCode?: string + codeLifetime: number + preAuthSessionId: string +} diff --git a/src/recipe/passwordless/utils.ts b/src/recipe/passwordless/utils.ts new file mode 100644 index 000000000..89f26a645 --- /dev/null +++ b/src/recipe/passwordless/utils.ts @@ -0,0 +1,177 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import parsePhoneNumber from 'libphonenumber-js/max' +import { NormalisedAppinfo } from '../../types' +import Recipe from './recipe' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import BackwardCompatibilityEmailService from './emaildelivery/services/backwardCompatibility' +import BackwardCompatibilitySmsService from './smsdelivery/services/backwardCompatibility' + +export function validateAndNormaliseUserInput( + _: Recipe, + appInfo: NormalisedAppinfo, + config: TypeInput, +): TypeNormalisedInput { + if ( + config.contactMethod !== 'PHONE' + && config.contactMethod !== 'EMAIL' + && config.contactMethod !== 'EMAIL_OR_PHONE' + ) + throw new Error('Please pass one of "PHONE", "EMAIL" or "EMAIL_OR_PHONE" as the contactMethod') + + if (config.flowType === undefined) + throw new Error('Please pass flowType argument in the config') + + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config.override, + } + + function getEmailDeliveryConfig() { + let emailService = config.emailDelivery?.service + + const createAndSendCustomEmail = config.contactMethod === 'PHONE' ? undefined : config.createAndSendCustomEmail + /** + * following code is for backward compatibility. + * if user has not passed emailDelivery config, we + * use the createAndSendCustomEmail config. If the user + * has not passed even that config, we use the default + * createAndSendCustomEmail implementation + */ + if (emailService === undefined) + emailService = new BackwardCompatibilityEmailService(appInfo, createAndSendCustomEmail) + + const emailDelivery = { + ...config.emailDelivery, + /** + * if we do + * let emailDelivery = { + * service: emailService, + * ...config.emailDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + service: emailService, + } + return emailDelivery + } + + function getSmsDeliveryConfig() { + let smsService = config.smsDelivery?.service + + const createAndSendCustomTextMessage + = config.contactMethod === 'EMAIL' ? undefined : config.createAndSendCustomTextMessage + /** + * following code is for backward compatibility. + * if user has not passed emailDelivery config, we + * use the createAndSendCustomTextMessage config. If the user + * has not passed even that config, we use the default + * createAndSendCustomTextMessage implementation + */ + if (smsService === undefined) + smsService = new BackwardCompatibilitySmsService(appInfo, createAndSendCustomTextMessage) + + const smsDelivery = { + ...config.smsDelivery, + /** + * if we do + * let smsDelivery = { + * service: smsService, + * ...config.smsDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + service: smsService, + } + return smsDelivery + } + if (config.contactMethod === 'EMAIL') { + return { + override, + getEmailDeliveryConfig, + getSmsDeliveryConfig, + flowType: config.flowType, + contactMethod: 'EMAIL', + validateEmailAddress: + config.validateEmailAddress === undefined ? defaultValidateEmail : config.validateEmailAddress, + getCustomUserInputCode: config.getCustomUserInputCode, + } + } + else if (config.contactMethod === 'PHONE') { + return { + override, + getEmailDeliveryConfig, + getSmsDeliveryConfig, + flowType: config.flowType, + contactMethod: 'PHONE', + validatePhoneNumber: + config.validatePhoneNumber === undefined ? defaultValidatePhoneNumber : config.validatePhoneNumber, + getCustomUserInputCode: config.getCustomUserInputCode, + } + } + else { + return { + override, + getEmailDeliveryConfig, + getSmsDeliveryConfig, + flowType: config.flowType, + contactMethod: 'EMAIL_OR_PHONE', + validateEmailAddress: + config.validateEmailAddress === undefined ? defaultValidateEmail : config.validateEmailAddress, + validatePhoneNumber: + config.validatePhoneNumber === undefined ? defaultValidatePhoneNumber : config.validatePhoneNumber, + getCustomUserInputCode: config.getCustomUserInputCode, + } + } +} + +export function defaultValidatePhoneNumber(value: string): Promise | string | undefined { + if (typeof value !== 'string') + return 'Development bug: Please make sure the phoneNumber field is a string' + + const parsedPhoneNumber = parsePhoneNumber(value) + if (parsedPhoneNumber === undefined || !parsedPhoneNumber.isValid()) + return 'Phone number is invalid' + + // we can even use Twilio's phone number validity lookup service: https://www.twilio.com/docs/glossary/what-e164. + + return undefined +} + +export function defaultValidateEmail(value: string): Promise | string | undefined { + // We check if the email syntax is correct + // As per https://github.com/supertokens/supertokens-auth-react/issues/5#issuecomment-709512438 + // Regex from https://stackoverflow.com/a/46181/3867175 + + if (typeof value !== 'string') + return 'Development bug: Please make sure the email field is a string' + + if ( + value.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ) === null + ) + return 'Email is invalid' + + return undefined +} diff --git a/src/recipe/session/accessToken.ts b/src/recipe/session/accessToken.ts new file mode 100644 index 000000000..c1af7532f --- /dev/null +++ b/src/recipe/session/accessToken.ts @@ -0,0 +1,109 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from './error' +import { ParsedJWTInfo, verifyJWT } from './jwt' + +export async function getInfoFromAccessToken( + jwtInfo: ParsedJWTInfo, + jwtSigningPublicKey: string, + doAntiCsrfCheck: boolean, +): Promise<{ + sessionHandle: string + userId: string + refreshTokenHash1: string + parentRefreshTokenHash1: string | undefined + userData: any + antiCsrfToken: string | undefined + expiryTime: number + timeCreated: number +}> { + try { + verifyJWT(jwtInfo, jwtSigningPublicKey) + const payload = jwtInfo.payload + + // This should be called before this function, but the check is very quick, so we can also do them here + validateAccessTokenStructure(payload) + + // We can mark these as defined (the ! after the calls), since validateAccessTokenPayload checks this + const sessionHandle = sanitizeStringInput(payload.sessionHandle)! + const userId = sanitizeStringInput(payload.userId)! + const refreshTokenHash1 = sanitizeStringInput(payload.refreshTokenHash1)! + const parentRefreshTokenHash1 = sanitizeStringInput(payload.parentRefreshTokenHash1) + const userData = payload.userData + const antiCsrfToken = sanitizeStringInput(payload.antiCsrfToken) + const expiryTime = sanitizeNumberInput(payload.expiryTime)! + const timeCreated = sanitizeNumberInput(payload.timeCreated)! + + if (antiCsrfToken === undefined && doAntiCsrfCheck) + throw new Error('Access token does not contain the anti-csrf token.') + + if (expiryTime < Date.now()) + throw new Error('Access token expired') + + return { + sessionHandle, + userId, + refreshTokenHash1, + parentRefreshTokenHash1, + userData, + antiCsrfToken, + expiryTime, + timeCreated, + } + } + catch (err) { + throw new STError({ + message: 'Failed to verify access token', + type: STError.TRY_REFRESH_TOKEN, + }) + } +} + +export function validateAccessTokenStructure(payload: any) { + if ( + typeof payload.sessionHandle !== 'string' + || typeof payload.userId !== 'string' + || typeof payload.refreshTokenHash1 !== 'string' + || payload.userData === undefined + || typeof payload.expiryTime !== 'number' + || typeof payload.timeCreated !== 'number' + ) { + // it would come here if we change the structure of the JWT. + throw new Error('Access token does not contain all the information. Maybe the structure has changed?') + } +} + +function sanitizeStringInput(field: any): string | undefined { + if (field === '') + return '' + + if (typeof field !== 'string') + return undefined + + try { + const result = field.trim() + return result + } + catch (err) {} + return undefined +} + +export function sanitizeNumberInput(field: any): number | undefined { + if (typeof field === 'number') + return field + + return undefined +} diff --git a/src/recipe/session/api/implementation.ts b/src/recipe/session/api/implementation.ts new file mode 100644 index 000000000..4be3fed8b --- /dev/null +++ b/src/recipe/session/api/implementation.ts @@ -0,0 +1,90 @@ +import { APIInterface, APIOptions, VerifySessionOptions } from '../' +import { normaliseHttpMethod } from '../../../utils' +import NormalisedURLPath from '../../../normalisedURLPath' +import { SessionContainerInterface } from '../types' +import { GeneralErrorResponse } from '../../../types' +import { getRequiredClaimValidators } from '../utils' + +export default function getAPIInterface(): APIInterface { + return { + async refreshPOST({ + options, + userContext, + }: { + options: APIOptions + userContext: any + }): Promise { + return await options.recipeImplementation.refreshSession({ + req: options.req, + res: options.res, + userContext, + }) + }, + + async verifySession({ + verifySessionOptions, + options, + userContext, + }: { + verifySessionOptions: VerifySessionOptions | undefined + options: APIOptions + userContext: any + }): Promise { + const method = normaliseHttpMethod(options.req.getMethod()) + if (method === 'options' || method === 'trace') + return undefined + + const incomingPath = new NormalisedURLPath(options.req.getOriginalURL()) + + const refreshTokenPath = options.config.refreshTokenPath + + if (incomingPath.equals(refreshTokenPath) && method === 'post') { + return options.recipeImplementation.refreshSession({ + req: options.req, + res: options.res, + userContext, + }) + } + else { + const session = await options.recipeImplementation.getSession({ + req: options.req, + res: options.res, + options: verifySessionOptions, + userContext, + }) + if (session !== undefined) { + const claimValidators = await getRequiredClaimValidators( + session, + verifySessionOptions?.overrideGlobalClaimValidators, + userContext, + ) + + await session.assertClaims(claimValidators, userContext) + } + + return session + } + }, + + async signOutPOST({ + session, + userContext, + }: { + options: APIOptions + session: SessionContainerInterface | undefined + userContext: any + }): Promise< + | { + status: 'OK' + } + | GeneralErrorResponse + > { + if (session !== undefined) + await session.revokeSession(userContext) + + return { + status: 'OK', + } + }, + } +} diff --git a/src/recipe/session/api/refresh.ts b/src/recipe/session/api/refresh.ts new file mode 100644 index 000000000..f458f2a05 --- /dev/null +++ b/src/recipe/session/api/refresh.ts @@ -0,0 +1,26 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../' + +export default async function handleRefreshAPI(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.refreshPOST === undefined) + return false + + await apiImplementation.refreshPOST({ options, userContext: makeDefaultUserContextFromAPI(options.req) }) + send200Response(options.res, {}) + return true +} diff --git a/src/recipe/session/api/signout.ts b/src/recipe/session/api/signout.ts new file mode 100644 index 000000000..7f502cde8 --- /dev/null +++ b/src/recipe/session/api/signout.ts @@ -0,0 +1,45 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../' + +export default async function signOutAPI(apiImplementation: APIInterface, options: APIOptions): Promise { + // Logic as per https://github.com/supertokens/supertokens-node/issues/34#issuecomment-717958537 + + if (apiImplementation.signOutPOST === undefined) + return false + + const defaultUserContext = makeDefaultUserContextFromAPI(options.req) + + const session = await options.recipeImplementation.getSession({ + req: options.req, + res: options.res, + options: { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }, + userContext: defaultUserContext, + }) + + const result = await apiImplementation.signOutPOST({ + options, + session, + userContext: defaultUserContext, + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/session/claimBaseClasses/booleanClaim.ts b/src/recipe/session/claimBaseClasses/booleanClaim.ts new file mode 100644 index 000000000..ca86d5382 --- /dev/null +++ b/src/recipe/session/claimBaseClasses/booleanClaim.ts @@ -0,0 +1,24 @@ +import { SessionClaim, SessionClaimValidator } from '../types' +import { PrimitiveClaim } from './primitiveClaim' + +export class BooleanClaim extends PrimitiveClaim { + constructor(conf: { + key: string + fetchValue: SessionClaim['fetchValue'] + defaultMaxAgeInSeconds?: number + }) { + super(conf) + + this.validators = { + ...this.validators, + isTrue: (maxAge?: number, id?: string): SessionClaimValidator => this.validators.hasValue(true, maxAge, id), + isFalse: (maxAge?: number, id?: string): SessionClaimValidator => + this.validators.hasValue(false, maxAge, id), + } + } + + validators!: PrimitiveClaim['validators'] & { + isTrue: (maxAge?: number, id?: string) => SessionClaimValidator + isFalse: (maxAge?: number, id?: string) => SessionClaimValidator + } +} diff --git a/src/recipe/session/claimBaseClasses/primitiveArrayClaim.ts b/src/recipe/session/claimBaseClasses/primitiveArrayClaim.ts new file mode 100644 index 000000000..a96b56dbc --- /dev/null +++ b/src/recipe/session/claimBaseClasses/primitiveArrayClaim.ts @@ -0,0 +1,227 @@ +import { JSONPrimitive } from '../../../types' +import { SessionClaim, SessionClaimValidator } from '../types' + +export class PrimitiveArrayClaim extends SessionClaim { + public readonly fetchValue: (userId: string, userContext: any) => Promise | T[] | undefined + public readonly defaultMaxAgeInSeconds: number | undefined + + constructor(config: { key: string; fetchValue: SessionClaim['fetchValue']; defaultMaxAgeInSeconds?: number }) { + super(config.key) + this.fetchValue = config.fetchValue + this.defaultMaxAgeInSeconds = config.defaultMaxAgeInSeconds + } + + addToPayload_internal(payload: any, value: T[], _userContext: any): any { + return { + ...payload, + [this.key]: { + v: value, + t: Date.now(), + }, + } + } + + removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any { + const res = { + ...payload, + [this.key]: null, + } + + return res + } + + removeFromPayload(payload: any, _userContext?: any): any { + const res = { + ...payload, + } + delete res[this.key] + + return res + } + + getValueFromPayload(payload: any, _userContext?: any): T[] | undefined { + return payload[this.key]?.v + } + + getLastRefetchTime(payload: any, _userContext?: any): number | undefined { + return payload[this.key]?.t + } + + validators = { + includes: ( + val: T, + maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, + id?: string, + ): SessionClaimValidator => { + return { + claim: this, + id: id ?? this.key, + shouldRefetch: (payload, ctx) => + this.getValueFromPayload(payload, ctx) === undefined + // We know payload[this.id] is defined since the value is not undefined in this branch + || (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx) + if (claimVal === undefined) { + return { + isValid: false, + reason: { message: 'value does not exist', expectedToInclude: val, actualValue: claimVal }, + } + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000 + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: 'expired', + ageInSeconds, + maxAgeInSeconds, + }, + } + } + if (!claimVal.includes(val)) { + return { + isValid: false, + reason: { message: 'wrong value', expectedToInclude: val, actualValue: claimVal }, + } + } + return { isValid: true } + }, + } + }, + excludes: ( + val: T, + maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, + id?: string, + ): SessionClaimValidator => { + return { + claim: this, + id: id ?? this.key, + shouldRefetch: (payload, ctx) => + this.getValueFromPayload(payload, ctx) === undefined + // We know payload[this.id] is defined since the value is not undefined in this branch + || (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx) + if (claimVal === undefined) { + return { + isValid: false, + reason: { + message: 'value does not exist', + expectedToNotInclude: val, + actualValue: claimVal, + }, + } + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000 + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: 'expired', + ageInSeconds, + maxAgeInSeconds, + }, + } + } + if (claimVal.includes(val)) { + return { + isValid: false, + reason: { message: 'wrong value', expectedToNotInclude: val, actualValue: claimVal }, + } + } + return { isValid: true } + }, + } + }, + includesAll: ( + val: T[], + maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, + id?: string, + ): SessionClaimValidator => { + return { + claim: this, + id: id ?? this.key, + shouldRefetch: (payload, ctx) => + this.getValueFromPayload(payload, ctx) === undefined + // We know payload[this.id] is defined since the value is not undefined in this branch + || (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx) + if (claimVal === undefined) { + return { + isValid: false, + reason: { message: 'value does not exist', expectedToInclude: val, actualValue: claimVal }, + } + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000 + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: 'expired', + ageInSeconds, + maxAgeInSeconds, + }, + } + } + const claimSet = new Set(claimVal) + const isValid = val.every(v => claimSet.has(v)) + return isValid + ? { isValid } + : { + isValid, + reason: { message: 'wrong value', expectedToInclude: val, actualValue: claimVal }, + } + }, + } + }, + excludesAll: ( + val: T[], + maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, + id?: string, + ): SessionClaimValidator => { + return { + claim: this, + id: id ?? this.key, + shouldRefetch: (payload, ctx) => + this.getValueFromPayload(payload, ctx) === undefined + // We know payload[this.id] is defined since the value is not undefined in this branch + || (maxAgeInSeconds !== undefined && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx) + if (claimVal === undefined) { + return { + isValid: false, + reason: { + message: 'value does not exist', + expectedToNotInclude: val, + actualValue: claimVal, + }, + } + } + + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000 + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: 'expired', + ageInSeconds, + maxAgeInSeconds, + }, + } + } + const claimSet = new Set(claimVal) + const isValid = val.every(v => !claimSet.has(v)) + return isValid + ? { isValid } + : { + isValid, + reason: { message: 'wrong value', expectedToNotInclude: val, actualValue: claimVal }, + } + }, + } + }, + } +} diff --git a/src/recipe/session/claimBaseClasses/primitiveClaim.ts b/src/recipe/session/claimBaseClasses/primitiveClaim.ts new file mode 100644 index 000000000..100283cee --- /dev/null +++ b/src/recipe/session/claimBaseClasses/primitiveClaim.ts @@ -0,0 +1,94 @@ +import { JSONPrimitive } from '../../../types' +import { SessionClaim, SessionClaimValidator } from '../types' + +export class PrimitiveClaim extends SessionClaim { + public readonly fetchValue: (userId: string, userContext: any) => Promise | T | undefined + public readonly defaultMaxAgeInSeconds: number | undefined + + constructor(config: { key: string; fetchValue: SessionClaim['fetchValue']; defaultMaxAgeInSeconds?: number }) { + super(config.key) + this.fetchValue = config.fetchValue + this.defaultMaxAgeInSeconds = config.defaultMaxAgeInSeconds + } + + addToPayload_internal(payload: any, value: T, _userContext: any): any { + return { + ...payload, + [this.key]: { + v: value, + t: Date.now(), + }, + } + } + + removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any { + const res = { + ...payload, + [this.key]: null, + } + + return res + } + + removeFromPayload(payload: any, _userContext?: any): any { + const res = { + ...payload, + } + delete res[this.key] + + return res + } + + getValueFromPayload(payload: any, _userContext?: any): T | undefined { + return payload[this.key]?.v + } + + getLastRefetchTime(payload: any, _userContext?: any): number | undefined { + return payload[this.key]?.t + } + + validators = { + hasValue: ( + val: T, + maxAgeInSeconds: number | undefined = this.defaultMaxAgeInSeconds, + id?: string, + ): SessionClaimValidator => { + return { + claim: this, + id: id ?? this.key, + shouldRefetch: (payload, ctx) => + this.getValueFromPayload(payload, ctx) === undefined + || (maxAgeInSeconds !== undefined // We know payload[this.id] is defined since the value is not undefined in this branch + && payload[this.key].t < Date.now() - maxAgeInSeconds * 1000), + validate: async (payload, ctx) => { + const claimVal = this.getValueFromPayload(payload, ctx) + if (claimVal === undefined) { + return { + isValid: false, + reason: { message: 'value does not exist', expectedValue: val, actualValue: claimVal }, + } + } + const ageInSeconds = (Date.now() - this.getLastRefetchTime(payload, ctx)!) / 1000 + if (maxAgeInSeconds !== undefined && ageInSeconds > maxAgeInSeconds) { + return { + isValid: false, + reason: { + message: 'expired', + ageInSeconds, + maxAgeInSeconds, + }, + } + } + + if (claimVal !== val) { + return { + isValid: false, + reason: { message: 'wrong value', expectedValue: val, actualValue: claimVal }, + } + } + return { isValid: true } + }, + } + }, + } +} diff --git a/src/recipe/session/claims.ts b/src/recipe/session/claims.ts new file mode 100644 index 000000000..fabfcb928 --- /dev/null +++ b/src/recipe/session/claims.ts @@ -0,0 +1,4 @@ +export { SessionClaim } from './types' +export { PrimitiveClaim } from './claimBaseClasses/primitiveClaim' +export { PrimitiveArrayClaim } from './claimBaseClasses/primitiveArrayClaim' +export { BooleanClaim } from './claimBaseClasses/booleanClaim' diff --git a/src/recipe/session/constants.ts b/src/recipe/session/constants.ts new file mode 100644 index 000000000..a6bd6a3ed --- /dev/null +++ b/src/recipe/session/constants.ts @@ -0,0 +1,21 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { TokenTransferMethod } from './types' + +export const REFRESH_API_PATH = '/session/refresh' +export const SIGNOUT_API_PATH = '/signout' + +export const availableTokenTransferMethods: TokenTransferMethod[] = ['cookie', 'header'] diff --git a/src/recipe/session/cookieAndHeaders.ts b/src/recipe/session/cookieAndHeaders.ts new file mode 100644 index 000000000..bc926cb4a --- /dev/null +++ b/src/recipe/session/cookieAndHeaders.ts @@ -0,0 +1,181 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { HEADER_RID } from '../../constants' +import { BaseRequest, BaseResponse } from '../../framework' +import { availableTokenTransferMethods } from './constants' +import { TokenTransferMethod, TokenType, TypeNormalisedInput } from './types' + +const authorizationHeaderKey = 'authorization' +const accessTokenCookieKey = 'sAccessToken' +const accessTokenHeaderKey = 'st-access-token' +const refreshTokenCookieKey = 'sRefreshToken' +const refreshTokenHeaderKey = 'st-refresh-token' + +const antiCsrfHeaderKey = 'anti-csrf' + +const frontTokenHeaderKey = 'front-token' + +const authModeHeaderKey = 'st-auth-mode' + +export function clearSessionFromAllTokenTransferMethods(config: TypeNormalisedInput, res: BaseResponse) { + // We are clearing the session in all transfermethods to be sure to override cookies in case they have been already added to the response. + // This is done to handle the following use-case: + // If the app overrides signInPOST to check the ban status of the user after the original implementation and throwing an UNAUTHORISED error + // In this case: the SDK has attached cookies to the response, but none was sent with the request + // We can't know which to clear since we can't reliably query or remove the set-cookie header added to the response (causes issues in some frameworks, i.e.: hapi) + // The safe solution in this case is to overwrite all the response cookies/headers with an empty value, which is what we are doing here + for (const transferMethod of availableTokenTransferMethods) + clearSession(config, res, transferMethod) +} + +export function clearSession(config: TypeNormalisedInput, res: BaseResponse, transferMethod: TokenTransferMethod) { + // If we can be specific about which transferMethod we want to clear, there is no reason to clear the other ones + const tokenTypes: TokenType[] = ['access', 'refresh'] + for (const token of tokenTypes) + setToken(config, res, token, '', 0, transferMethod) + + res.removeHeader(antiCsrfHeaderKey) + // This can be added multiple times in some cases, but that should be OK + res.setHeader(frontTokenHeaderKey, 'remove', false) + res.setHeader('Access-Control-Expose-Headers', frontTokenHeaderKey, true) +} + +export function getAntiCsrfTokenFromHeaders(req: BaseRequest): string | undefined { + return req.getHeaderValue(antiCsrfHeaderKey) +} + +export function setAntiCsrfTokenInHeaders(res: BaseResponse, antiCsrfToken: string) { + res.setHeader(antiCsrfHeaderKey, antiCsrfToken, false) + res.setHeader('Access-Control-Expose-Headers', antiCsrfHeaderKey, true) +} + +export function setFrontTokenInHeaders(res: BaseResponse, userId: string, atExpiry: number, accessTokenPayload: any) { + const tokenInfo = { + uid: userId, + ate: atExpiry, + up: accessTokenPayload, + } + res.setHeader(frontTokenHeaderKey, Buffer.from(JSON.stringify(tokenInfo)).toString('base64'), false) + res.setHeader('Access-Control-Expose-Headers', frontTokenHeaderKey, true) +} + +export function getCORSAllowedHeaders(): string[] { + return [antiCsrfHeaderKey, HEADER_RID, authorizationHeaderKey, authModeHeaderKey] +} + +function getCookieNameFromTokenType(tokenType: TokenType) { + switch (tokenType) { + case 'access': + return accessTokenCookieKey + case 'refresh': + return refreshTokenCookieKey + default: + throw new Error('Unknown token type, should never happen.') + } +} + +function getResponseHeaderNameForTokenType(tokenType: TokenType) { + switch (tokenType) { + case 'access': + return accessTokenHeaderKey + case 'refresh': + return refreshTokenHeaderKey + default: + throw new Error('Unknown token type, should never happen.') + } +} + +export function getToken(req: BaseRequest, tokenType: TokenType, transferMethod: TokenTransferMethod) { + if (transferMethod === 'cookie') { + return req.getCookieValue(getCookieNameFromTokenType(tokenType)) + } + else if (transferMethod === 'header') { + const value = req.getHeaderValue(authorizationHeaderKey) + if (value === undefined || !value.startsWith('Bearer ')) + return undefined + + return value.replace(/^Bearer /, '').trim() + } + else { + throw new Error(`Should never happen: Unknown transferMethod: ${transferMethod}`) + } +} + +export function setToken( + config: TypeNormalisedInput, + res: BaseResponse, + tokenType: TokenType, + value: string, + expires: number, + transferMethod: TokenTransferMethod, +) { + if (transferMethod === 'cookie') { + setCookie( + config, + res, + getCookieNameFromTokenType(tokenType), + value, + expires, + tokenType === 'refresh' ? 'refreshTokenPath' : 'accessTokenPath', + ) + } + else if (transferMethod === 'header') { + setHeader(res, getResponseHeaderNameForTokenType(tokenType), value) + } +} + +export function setHeader(res: BaseResponse, name: string, value: string) { + res.setHeader(name, value, false) + res.setHeader('Access-Control-Expose-Headers', name, true) +} + +/** + * + * @param res + * @param name + * @param value + * @param domain + * @param secure + * @param httpOnly + * @param expires + * @param path + */ +export function setCookie( + config: TypeNormalisedInput, + res: BaseResponse, + name: string, + value: string, + expires: number, + pathType: 'refreshTokenPath' | 'accessTokenPath', +) { + const domain = config.cookieDomain + const secure = config.cookieSecure + const sameSite = config.cookieSameSite + let path = '' + if (pathType === 'refreshTokenPath') { + path = config.refreshTokenPath.getAsStringDangerous() + } + else if (pathType === 'accessTokenPath') { + path + = config.accessTokenPath.getAsStringDangerous() === '' ? '/' : config.accessTokenPath.getAsStringDangerous() + } + const httpOnly = true + + return res.setCookie(name, value, domain, secure, httpOnly, expires, path, sameSite) +} + +export function getAuthModeFromHeader(req: BaseRequest): string | undefined { + return req.getHeaderValue(authModeHeaderKey)?.toLowerCase() +} diff --git a/src/recipe/session/error.ts b/src/recipe/session/error.ts new file mode 100644 index 000000000..a2e47a1f2 --- /dev/null +++ b/src/recipe/session/error.ts @@ -0,0 +1,64 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../../error' +import { ClaimValidationError } from './types' + +export default class SessionError extends STError { + static UNAUTHORISED = 'UNAUTHORISED' as const + static TRY_REFRESH_TOKEN = 'TRY_REFRESH_TOKEN' as const + static TOKEN_THEFT_DETECTED = 'TOKEN_THEFT_DETECTED' as const + static INVALID_CLAIMS = 'INVALID_CLAIMS' as const + + constructor( + options: + | { + message: string + type: 'UNAUTHORISED' + payload?: { + clearTokens: boolean + } + } + | { + message: string + type: 'TRY_REFRESH_TOKEN' + } + | { + message: string + type: 'TOKEN_THEFT_DETECTED' + payload: { + userId: string + sessionHandle: string + } + } + | { + message: string + type: 'INVALID_CLAIMS' + payload: ClaimValidationError[] + }, + ) { + super( + (options.type === 'UNAUTHORISED' && options.payload === undefined) + ? { + ...options, + payload: { + clearTokens: true, + }, + } + : { ...options }, + ) + this.fromRecipe = 'session' + } +} diff --git a/src/recipe/session/framework/awsLambda.ts b/src/recipe/session/framework/awsLambda.ts new file mode 100644 index 000000000..67fb15629 --- /dev/null +++ b/src/recipe/session/framework/awsLambda.ts @@ -0,0 +1,41 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Callback, Context, Handler } from 'aws-lambda' +import { AWSRequest, AWSResponse } from '../../../framework/awsLambda/framework' +import type { SessionEvent, SessionEventV2 } from '../../../framework/awsLambda/framework' +import SuperTokens from '../../../supertokens' +import Session from '../recipe' +import { VerifySessionOptions } from '..' + +export function verifySession(handler: Handler, verifySessionOptions?: VerifySessionOptions): Handler { + return async (event: SessionEvent | SessionEventV2, context: Context, callback: Callback) => { + const supertokens = SuperTokens.getInstanceOrThrowError() + const request = new AWSRequest(event) + const response = new AWSResponse(event) + try { + const sessionRecipe = Session.getInstanceOrThrowError() + event.session = await sessionRecipe.verifySession(verifySessionOptions, request, response) + const handlerResult = await handler(event, context, callback) + return response.sendResponse(handlerResult) + } + catch (err) { + await supertokens.errorHandler(err, request, response) + if (response.responseSet) + return response.sendResponse({}) + + throw err + } + } +} diff --git a/src/recipe/session/framework/express.ts b/src/recipe/session/framework/express.ts new file mode 100644 index 000000000..a32df1797 --- /dev/null +++ b/src/recipe/session/framework/express.ts @@ -0,0 +1,41 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { NextFunction, Response } from 'express' +import Session from '../recipe' +import type { VerifySessionOptions } from '..' +import type { SessionRequest } from '../../../framework/express/framework' +import { ExpressRequest, ExpressResponse } from '../../../framework/express/framework' +import SuperTokens from '../../../supertokens' + +export function verifySession(options?: VerifySessionOptions) { + return async (req: SessionRequest, res: Response, next: NextFunction) => { + const request = new ExpressRequest(req) + const response = new ExpressResponse(res) + try { + const sessionRecipe = Session.getInstanceOrThrowError() + req.session = await sessionRecipe.verifySession(options, request, response) + next() + } + catch (err) { + try { + const supertokens = SuperTokens.getInstanceOrThrowError() + await supertokens.errorHandler(err, request, response) + } + catch { + next(err) + } + } + } +} diff --git a/src/recipe/session/framework/fastify.ts b/src/recipe/session/framework/fastify.ts new file mode 100644 index 000000000..4c2f6d590 --- /dev/null +++ b/src/recipe/session/framework/fastify.ts @@ -0,0 +1,35 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { FastifyReply } from 'fastify' +import Session from '../recipe' +import { VerifySessionOptions } from '..' +import { FastifyRequest, FastifyResponse, SessionRequest } from '../../../framework/fastify/framework' +import SuperTokens from '../../../supertokens' + +export function verifySession(options?: VerifySessionOptions) { + return async (req: SessionRequest, res: FastifyReply) => { + const sessionRecipe = Session.getInstanceOrThrowError() + const request = new FastifyRequest(req) + const response = new FastifyResponse(res) + try { + req.session = await sessionRecipe.verifySession(options, request, response) + } + catch (err) { + const supertokens = SuperTokens.getInstanceOrThrowError() + await supertokens.errorHandler(err, request, response) + throw err + } + } +} diff --git a/src/recipe/session/framework/hapi.ts b/src/recipe/session/framework/hapi.ts new file mode 100644 index 000000000..7941fad25 --- /dev/null +++ b/src/recipe/session/framework/hapi.ts @@ -0,0 +1,28 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { ResponseToolkit } from '@hapi/hapi' +import Session from '../recipe' +import { VerifySessionOptions } from '..' +import { ExtendedResponseToolkit, HapiRequest, HapiResponse, SessionRequest } from '../../../framework/hapi/framework' + +export function verifySession(options?: VerifySessionOptions) { + return async (req: SessionRequest, h: ResponseToolkit) => { + const sessionRecipe = Session.getInstanceOrThrowError() + const request = new HapiRequest(req) + const response = new HapiResponse(h as ExtendedResponseToolkit) + req.session = await sessionRecipe.verifySession(options, request, response) + return h.continue + } +} diff --git a/src/recipe/session/framework/index.ts b/src/recipe/session/framework/index.ts new file mode 100644 index 000000000..b219aeb28 --- /dev/null +++ b/src/recipe/session/framework/index.ts @@ -0,0 +1,36 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import * as expressFramework from './express' +import * as fastifyFramework from './fastify' +import * as hapiFramework from './hapi' +import * as loopbackFramework from './loopback' +import * as koaFramework from './koa' +import * as awsLambdaFramework from './awsLambda' + +export default { + express: expressFramework, + fastify: fastifyFramework, + hapi: hapiFramework, + loopback: loopbackFramework, + koa: koaFramework, + awsLambda: awsLambdaFramework, +} + +export const express = expressFramework +export const fastify = fastifyFramework +export const hapi = hapiFramework +export const loopback = loopbackFramework +export const koa = koaFramework +export const awsLambda = awsLambdaFramework diff --git a/src/recipe/session/framework/koa.ts b/src/recipe/session/framework/koa.ts new file mode 100644 index 000000000..4b4d2f33f --- /dev/null +++ b/src/recipe/session/framework/koa.ts @@ -0,0 +1,29 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import type { Next } from 'koa' +import Session from '../recipe' +import type { VerifySessionOptions } from '..' +import { KoaRequest, KoaResponse } from '../../../framework/koa/framework' +import type { SessionContext } from '../../../framework/koa/framework' + +export function verifySession(options?: VerifySessionOptions) { + return async (ctx: SessionContext, next: Next) => { + const sessionRecipe = Session.getInstanceOrThrowError() + const request = new KoaRequest(ctx) + const response = new KoaResponse(ctx) + ctx.session = await sessionRecipe.verifySession(options, request, response) + await next() + } +} diff --git a/src/recipe/session/framework/loopback.ts b/src/recipe/session/framework/loopback.ts new file mode 100644 index 000000000..344f84d87 --- /dev/null +++ b/src/recipe/session/framework/loopback.ts @@ -0,0 +1,31 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { InterceptorOrKey, InvocationContext, Next } from '@loopback/core' +import { MiddlewareContext } from '@loopback/rest' +import Session from '../recipe' +import { VerifySessionOptions } from '..' +import type { SessionContext as Context } from '../../../framework/loopback/framework' +import { LoopbackRequest, LoopbackResponse } from '../../../framework/loopback/framework' + +export function verifySession(options?: VerifySessionOptions): InterceptorOrKey { + return async (ctx: InvocationContext, next: Next) => { + const sessionRecipe = Session.getInstanceOrThrowError() + const middlewareCtx = await ctx.get('middleware.http.context') + const request = new LoopbackRequest(middlewareCtx) + const response = new LoopbackResponse(middlewareCtx); + (middlewareCtx as Context).session = await sessionRecipe.verifySession(options, request, response) + return await next() + } +} diff --git a/src/recipe/session/index.ts b/src/recipe/session/index.ts new file mode 100644 index 000000000..c17f366f9 --- /dev/null +++ b/src/recipe/session/index.ts @@ -0,0 +1,420 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OpenIdRecipe from '../openid/recipe' +import { JSONObject } from '../../types' +import frameworks from '../../framework' +import SuperTokens from '../../supertokens' +import Recipe from './recipe' +import { + APIInterface, + APIOptions, + ClaimValidationError, + RecipeInterface, + SessionClaim, + SessionClaimValidator, + SessionContainerInterface as SessionContainer, + SessionInformation, + VerifySessionOptions, +} from './types' +import SuperTokensError from './error' +import { getRequiredClaimValidators } from './utils' + +// For Express +export default class SessionWrapper { + static init = Recipe.init + + static Error = SuperTokensError + + static async createNewSession( + req: any, + res: any, + userId: string, + accessTokenPayload: any = {}, + sessionData: any = {}, + userContext: any = {}, + ) { + const claimsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimsAddedByOtherRecipes() + + let finalAccessTokenPayload = accessTokenPayload + + for (const claim of claimsAddedByOtherRecipes) { + const update = await claim.build(userId, userContext) + finalAccessTokenPayload = { + ...finalAccessTokenPayload, + ...update, + } + } + + if (!req.wrapperUsed) + req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req) + + if (!res.wrapperUsed) + res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res) + + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewSession({ + req, + res, + userId, + accessTokenPayload: finalAccessTokenPayload, + sessionData, + userContext, + }) + } + + static async validateClaimsForSessionHandle( + sessionHandle: string, + overrideGlobalClaimValidators?: ( + globalClaimValidators: SessionClaimValidator[], + sessionInfo: SessionInformation, + userContext: any + ) => Promise | SessionClaimValidator[], + userContext: any = {}, + ): Promise< + | { + status: 'SESSION_DOES_NOT_EXIST_ERROR' + } + | { + status: 'OK' + invalidClaims: ClaimValidationError[] + } + > { + const recipeImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl + + const sessionInfo = await recipeImpl.getSessionInformation({ + sessionHandle, + userContext, + }) + if (sessionInfo === undefined) { + return { + status: 'SESSION_DOES_NOT_EXIST_ERROR', + } + } + + const claimValidatorsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes() + const globalClaimValidators: SessionClaimValidator[] = await recipeImpl.getGlobalClaimValidators({ + userId: sessionInfo?.userId, + claimValidatorsAddedByOtherRecipes, + userContext, + }) + + const claimValidators + = overrideGlobalClaimValidators !== undefined + ? await overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, userContext) + : globalClaimValidators + + const claimValidationResponse = await recipeImpl.validateClaims({ + userId: sessionInfo.userId, + accessTokenPayload: sessionInfo.accessTokenPayload, + claimValidators, + userContext, + }) + + if (claimValidationResponse.accessTokenPayloadUpdate !== undefined) { + if ( + !(await recipeImpl.mergeIntoAccessTokenPayload({ + sessionHandle, + accessTokenPayloadUpdate: claimValidationResponse.accessTokenPayloadUpdate, + userContext, + })) + ) { + return { + status: 'SESSION_DOES_NOT_EXIST_ERROR', + } + } + } + return { + status: 'OK', + invalidClaims: claimValidationResponse.invalidClaims, + } + } + + static async validateClaimsInJWTPayload( + userId: string, + jwtPayload: JSONObject, + overrideGlobalClaimValidators?: ( + globalClaimValidators: SessionClaimValidator[], + userId: string, + userContext: any + ) => Promise | SessionClaimValidator[], + userContext: any = {}, + ): Promise<{ + status: 'OK' + invalidClaims: ClaimValidationError[] + }> { + const recipeImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl + + const claimValidatorsAddedByOtherRecipes = Recipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes() + const globalClaimValidators: SessionClaimValidator[] = await recipeImpl.getGlobalClaimValidators({ + userId, + claimValidatorsAddedByOtherRecipes, + userContext, + }) + + const claimValidators + = overrideGlobalClaimValidators !== undefined + ? await overrideGlobalClaimValidators(globalClaimValidators, userId, userContext) + : globalClaimValidators + return recipeImpl.validateClaimsInJWTPayload({ + userId, + jwtPayload, + claimValidators, + userContext, + }) + } + + static getSession(req: any, res: any): Promise + static getSession( + req: any, + res: any, + options?: VerifySessionOptions & { sessionRequired?: true }, + userContext?: any + ): Promise + static getSession( + req: any, + res: any, + options?: VerifySessionOptions & { sessionRequired: false }, + userContext?: any + ): Promise + static async getSession(req: any, res: any, options?: VerifySessionOptions, userContext: any = {}) { + if (!res.wrapperUsed) + res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res) + + if (!req.wrapperUsed) + req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req) + + const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl + const session = await recipeInterfaceImpl.getSession({ req, res, options, userContext }) + + if (session !== undefined) { + const claimValidators = await getRequiredClaimValidators( + session, + options?.overrideGlobalClaimValidators, + userContext, + ) + await session.assertClaims(claimValidators, userContext) + } + return session + } + + static getSessionInformation(sessionHandle: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getSessionInformation({ + sessionHandle, + userContext, + }) + } + + static refreshSession(req: any, res: any, userContext: any = {}) { + if (!res.wrapperUsed) + res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res) + + if (!req.wrapperUsed) + req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req) + + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.refreshSession({ req, res, userContext }) + } + + static revokeAllSessionsForUser(userId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllSessionsForUser({ userId, userContext }) + } + + static getAllSessionHandlesForUser(userId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getAllSessionHandlesForUser({ + userId, + userContext, + }) + } + + static revokeSession(sessionHandle: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeSession({ sessionHandle, userContext }) + } + + static revokeMultipleSessions(sessionHandles: string[], userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeMultipleSessions({ + sessionHandles, + userContext, + }) + } + + static updateSessionData(sessionHandle: string, newSessionData: any, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateSessionData({ + sessionHandle, + newSessionData, + userContext, + }) + } + + static regenerateAccessToken(accessToken: string, newAccessTokenPayload?: any, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.regenerateAccessToken({ + accessToken, + newAccessTokenPayload, + userContext, + }) + } + + static updateAccessTokenPayload(sessionHandle: string, newAccessTokenPayload: any, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateAccessTokenPayload({ + sessionHandle, + newAccessTokenPayload, + userContext, + }) + } + + static mergeIntoAccessTokenPayload( + sessionHandle: string, + accessTokenPayloadUpdate: JSONObject, + userContext: any = {}, + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.mergeIntoAccessTokenPayload({ + sessionHandle, + accessTokenPayloadUpdate, + userContext, + }) + } + + static createJWT(payload?: any, validitySeconds?: number, userContext: any = {}) { + const openIdRecipe: OpenIdRecipe | undefined = Recipe.getInstanceOrThrowError().openIdRecipe + + if (openIdRecipe !== undefined) + return openIdRecipe.recipeImplementation.createJWT({ payload, validitySeconds, userContext }) + + throw new global.Error( + 'createJWT cannot be used without enabling the JWT feature. Please set \'enableJWT: true\' when initialising the Session recipe', + ) + } + + static getJWKS(userContext: any = {}) { + const openIdRecipe: OpenIdRecipe | undefined = Recipe.getInstanceOrThrowError().openIdRecipe + + if (openIdRecipe !== undefined) + return openIdRecipe.recipeImplementation.getJWKS({ userContext }) + + throw new global.Error( + 'getJWKS cannot be used without enabling the JWT feature. Please set \'enableJWT: true\' when initialising the Session recipe', + ) + } + + static getOpenIdDiscoveryConfiguration(userContext: any = {}) { + const openIdRecipe: OpenIdRecipe | undefined = Recipe.getInstanceOrThrowError().openIdRecipe + + if (openIdRecipe !== undefined) + return openIdRecipe.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }) + + throw new global.Error( + 'getOpenIdDiscoveryConfiguration cannot be used without enabling the JWT feature. Please set \'enableJWT: true\' when initialising the Session recipe', + ) + } + + static fetchAndSetClaim(sessionHandle: string, claim: SessionClaim, userContext: any = {}): Promise { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.fetchAndSetClaim({ + sessionHandle, + claim, + userContext, + }) + } + + static setClaimValue( + sessionHandle: string, + claim: SessionClaim, + value: T, + userContext: any = {}, + ): Promise { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.setClaimValue({ + sessionHandle, + claim, + value, + userContext, + }) + } + + static getClaimValue( + sessionHandle: string, + claim: SessionClaim, + userContext: any = {}, + ): Promise< + | { + status: 'SESSION_DOES_NOT_EXIST_ERROR' + } + | { + status: 'OK' + value: T | undefined + } + > { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getClaimValue({ + sessionHandle, + claim, + userContext, + }) + } + + static removeClaim(sessionHandle: string, claim: SessionClaim, userContext: any = {}): Promise { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removeClaim({ + sessionHandle, + claim, + userContext, + }) + } +} + +export const init = SessionWrapper.init + +export const createNewSession = SessionWrapper.createNewSession + +export const getSession = SessionWrapper.getSession + +export const getSessionInformation = SessionWrapper.getSessionInformation + +export const refreshSession = SessionWrapper.refreshSession + +export const revokeAllSessionsForUser = SessionWrapper.revokeAllSessionsForUser + +export const getAllSessionHandlesForUser = SessionWrapper.getAllSessionHandlesForUser + +export const revokeSession = SessionWrapper.revokeSession + +export const revokeMultipleSessions = SessionWrapper.revokeMultipleSessions + +export const updateSessionData = SessionWrapper.updateSessionData + +export const updateAccessTokenPayload = SessionWrapper.updateAccessTokenPayload +export const mergeIntoAccessTokenPayload = SessionWrapper.mergeIntoAccessTokenPayload + +export const fetchAndSetClaim = SessionWrapper.fetchAndSetClaim +export const setClaimValue = SessionWrapper.setClaimValue +export const getClaimValue = SessionWrapper.getClaimValue +export const removeClaim = SessionWrapper.removeClaim +export const validateClaimsInJWTPayload = SessionWrapper.validateClaimsInJWTPayload +export const validateClaimsForSessionHandle = SessionWrapper.validateClaimsForSessionHandle + +export const Error = SessionWrapper.Error + +// JWT Functions +export const createJWT = SessionWrapper.createJWT + +export const getJWKS = SessionWrapper.getJWKS + +// Open id functions + +export const getOpenIdDiscoveryConfiguration = SessionWrapper.getOpenIdDiscoveryConfiguration + +export type { + VerifySessionOptions, + RecipeInterface, + SessionContainer, + APIInterface, + APIOptions, + SessionInformation, + SessionClaimValidator, +} diff --git a/src/recipe/session/jwt.ts b/src/recipe/session/jwt.ts new file mode 100644 index 000000000..eff7e8ba1 --- /dev/null +++ b/src/recipe/session/jwt.ts @@ -0,0 +1,75 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import * as crypto from 'crypto' + +const HEADERS = new Set([ + Buffer.from( + JSON.stringify({ + alg: 'RS256', + typ: 'JWT', + version: '1', + }), + ).toString('base64'), + Buffer.from( + JSON.stringify({ + alg: 'RS256', + typ: 'JWT', + version: '2', + }), + ).toString('base64'), +]) + +export interface ParsedJWTInfo { + rawTokenString: string + rawPayload: string + header: string + payload: any + signature: string +} + +export function parseJWTWithoutSignatureVerification(jwt: string): ParsedJWTInfo { + const splittedInput = jwt.split('.') + if (splittedInput.length !== 3) + throw new Error('Invalid JWT') + + // checking header + if (!HEADERS.has(splittedInput[0])) + throw new Error('JWT header mismatch') + + return { + rawTokenString: jwt, + rawPayload: splittedInput[1], + header: splittedInput[0], + // Ideally we would only parse this after the signature verification is done. + // We do this at the start, since we want to check if a token can be a supertokens access token or not + payload: JSON.parse(Buffer.from(splittedInput[1], 'base64').toString()), + signature: splittedInput[2], + } +} + +export function verifyJWT({ header, rawPayload, signature }: ParsedJWTInfo, jwtSigningPublicKey: string): void { + const verifier = crypto.createVerify('sha256') + // convert the jwtSigningPublicKey into .pem format + + verifier.update(`${header}.${rawPayload}`) + if ( + !verifier.verify( + `-----BEGIN PUBLIC KEY-----\n${jwtSigningPublicKey}\n-----END PUBLIC KEY-----`, + signature, + 'base64', + ) + ) + throw new Error('JWT verification failed') +} diff --git a/src/recipe/session/recipe.ts b/src/recipe/session/recipe.ts new file mode 100644 index 000000000..1549844a2 --- /dev/null +++ b/src/recipe/session/recipe.ts @@ -0,0 +1,296 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import OpenIdRecipe from '../openid/recipe' +import { logDebugMessage } from '../../logger' +import { makeDefaultUserContextFromAPI } from '../../utils' +import { + APIInterface, + RecipeInterface, + SessionClaim, + SessionClaimValidator, + TypeInput, + TypeNormalisedInput, + VerifySessionOptions, +} from './types' +import STError from './error' +import { validateAndNormaliseUserInput } from './utils' +import handleRefreshAPI from './api/refresh' +import signOutAPI from './api/signout' +import { REFRESH_API_PATH, SIGNOUT_API_PATH } from './constants' +import { + clearSessionFromAllTokenTransferMethods, + getCORSAllowedHeaders as getCORSAllowedHeadersFromCookiesAndHeaders, +} from './cookieAndHeaders' +import RecipeImplementation from './recipeImplementation' +import RecipeImplementationWithJWT from './with-jwt' +import APIImplementation from './api/implementation' +import { APIOptions } from '.' + +// For Express +export default class SessionRecipe extends RecipeModule { + private static instance: SessionRecipe | undefined = undefined + static RECIPE_ID = 'session' + + private claimsAddedByOtherRecipes: SessionClaim[] = [] + private claimValidatorsAddedByOtherRecipes: SessionClaimValidator[] = [] + + config: TypeNormalisedInput + + recipeInterfaceImpl: RecipeInterface + openIdRecipe?: OpenIdRecipe + + apiImpl: APIInterface + + isInServerlessEnv: boolean + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo) + this.config = validateAndNormaliseUserInput(this, appInfo, config) + logDebugMessage(`session init: antiCsrf: ${this.config.antiCsrf}`) + logDebugMessage(`session init: cookieDomain: ${this.config.cookieDomain}`) + logDebugMessage(`session init: cookieSameSite: ${this.config.cookieSameSite}`) + logDebugMessage(`session init: cookieSecure: ${this.config.cookieSecure}`) + logDebugMessage(`session init: refreshTokenPath: ${this.config.refreshTokenPath.getAsStringDangerous()}`) + logDebugMessage(`session init: sessionExpiredStatusCode: ${this.config.sessionExpiredStatusCode}`) + + this.isInServerlessEnv = isInServerlessEnv + + if (this.config.jwt.enable === true) { + this.openIdRecipe = new OpenIdRecipe(recipeId, appInfo, isInServerlessEnv, { + issuer: this.config.jwt.issuer, + override: this.config.override.openIdFeature, + }) + + const builder = new OverrideableBuilder( + RecipeImplementation( + Querier.getNewInstanceOrThrowError(recipeId), + this.config, + this.getAppInfo(), + () => this.recipeInterfaceImpl, + ), + ) + this.recipeInterfaceImpl = builder + .override((oI) => { + return RecipeImplementationWithJWT( + oI, + // this.jwtRecipe is never undefined here + this.openIdRecipe!.recipeImplementation, + this.config, + ) + }) + .override(this.config.override.functions) + .build() + } + else { + // eslint-disable-next-line no-lone-blocks + { + const builder = new OverrideableBuilder( + RecipeImplementation( + Querier.getNewInstanceOrThrowError(recipeId), + this.config, + this.getAppInfo(), + () => this.recipeInterfaceImpl, + ), + ) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + } + + static getInstanceOrThrowError(): SessionRecipe { + if (SessionRecipe.instance !== undefined) + return SessionRecipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (SessionRecipe.instance === undefined) { + SessionRecipe.instance = new SessionRecipe(SessionRecipe.RECIPE_ID, appInfo, isInServerlessEnv, config) + return SessionRecipe.instance + } + else { + throw new Error('Session recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + SessionRecipe.instance = undefined + } + + addClaimFromOtherRecipe = (claim: SessionClaim) => { + // We are throwing here (and not in addClaimValidatorFromOtherRecipe) because if multiple + // claims are added with the same key they will overwrite each other. Validators will all run + // and work as expected even if they are added multiple times. + if (this.claimsAddedByOtherRecipes.some(c => c.key === claim.key)) + throw new Error('Claim added by multiple recipes') + + this.claimsAddedByOtherRecipes.push(claim) + } + + getClaimsAddedByOtherRecipes = (): SessionClaim[] => { + return this.claimsAddedByOtherRecipes + } + + addClaimValidatorFromOtherRecipe = (builder: SessionClaimValidator) => { + this.claimValidatorsAddedByOtherRecipes.push(builder) + } + + getClaimValidatorsAddedByOtherRecipes = (): SessionClaimValidator[] => { + return this.claimValidatorsAddedByOtherRecipes + } + + // abstract instance functions below............... + + getAPIsHandled = (): APIHandled[] => { + const apisHandled: APIHandled[] = [ + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(REFRESH_API_PATH), + id: REFRESH_API_PATH, + disabled: this.apiImpl.refreshPOST === undefined, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(SIGNOUT_API_PATH), + id: SIGNOUT_API_PATH, + disabled: this.apiImpl.signOutPOST === undefined, + }, + ] + + if (this.openIdRecipe !== undefined) + apisHandled.push(...this.openIdRecipe.getAPIsHandled()) + + return apisHandled + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + path: NormalisedURLPath, + method: HTTPMethod, + ): Promise => { + const options: APIOptions = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + } + if (id === REFRESH_API_PATH) + return await handleRefreshAPI(this.apiImpl, options) + else if (id === SIGNOUT_API_PATH) + return await signOutAPI(this.apiImpl, options) + else if (this.openIdRecipe !== undefined) + return await this.openIdRecipe.handleAPIRequest(id, req, res, path, method) + else + return false + } + + handleError = async (err: STError, request: BaseRequest, response: BaseResponse) => { + if (err.fromRecipe === SessionRecipe.RECIPE_ID) { + if (err.type === STError.UNAUTHORISED) { + logDebugMessage('errorHandler: returning UNAUTHORISED') + if ( + err.payload === undefined + || err.payload.clearTokens === undefined + || err.payload.clearTokens === true + ) { + logDebugMessage('errorHandler: Clearing tokens because of UNAUTHORISED response') + clearSessionFromAllTokenTransferMethods(this.config, response) + } + return await this.config.errorHandlers.onUnauthorised(err.message, request, response) + } + else if (err.type === STError.TRY_REFRESH_TOKEN) { + logDebugMessage('errorHandler: returning TRY_REFRESH_TOKEN') + return await this.config.errorHandlers.onTryRefreshToken(err.message, request, response) + } + else if (err.type === STError.TOKEN_THEFT_DETECTED) { + logDebugMessage('errorHandler: returning TOKEN_THEFT_DETECTED') + logDebugMessage('errorHandler: Clearing tokens because of TOKEN_THEFT_DETECTED response') + clearSessionFromAllTokenTransferMethods(this.config, response) + return await this.config.errorHandlers.onTokenTheftDetected( + err.payload.sessionHandle, + err.payload.userId, + request, + response, + ) + } + else if (err.type === STError.INVALID_CLAIMS) { + return await this.config.errorHandlers.onInvalidClaim(err.payload, request, response) + } + else { + throw err + } + } + else if (this.openIdRecipe !== undefined) { + return await this.openIdRecipe.handleError(err, request, response) + } + else { + throw err + } + } + + getAllCORSHeaders = (): string[] => { + const corsHeaders: string[] = [...getCORSAllowedHeadersFromCookiesAndHeaders()] + + if (this.openIdRecipe !== undefined) + corsHeaders.push(...this.openIdRecipe.getAllCORSHeaders()) + + return corsHeaders + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return ( + STError.isErrorFromSuperTokens(err) + && (err.fromRecipe === SessionRecipe.RECIPE_ID + || (this.openIdRecipe !== undefined && this.openIdRecipe.isErrorFromThisRecipe(err))) + ) + } + + verifySession = async (options: VerifySessionOptions | undefined, request: BaseRequest, response: BaseResponse) => { + return await this.apiImpl.verifySession({ + verifySessionOptions: options, + options: { + config: this.config, + req: request, + res: response, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + }, + userContext: makeDefaultUserContextFromAPI(request), + }) + } +} diff --git a/src/recipe/session/recipeImplementation.ts b/src/recipe/session/recipeImplementation.ts new file mode 100644 index 000000000..fce16dc99 --- /dev/null +++ b/src/recipe/session/recipeImplementation.ts @@ -0,0 +1,747 @@ +import { getRidFromHeader, isAnIpAddress, normaliseHttpMethod } from '../../utils' +import { Querier } from '../../querier' +import { PROCESS_STATE, ProcessState } from '../../processState' +import NormalisedURLPath from '../../normalisedURLPath' +import { JSONObject, NormalisedAppinfo } from '../../types' +import { logDebugMessage } from '../../logger' +import { BaseResponse } from '../../framework/response' +import { BaseRequest } from '../../framework/request' +import STError from './error' +import Session from './sessionClass' +import { attachTokensToResponse, validateClaimsInPayload } from './utils' +import { + clearSession, + getAntiCsrfTokenFromHeaders, + getToken, + setCookie, + setFrontTokenInHeaders, + setToken, +} from './cookieAndHeaders' +import * as SessionFunctions from './sessionFunctions' +import { + AntiCsrfType, + ClaimValidationError, + KeyInfo, + RecipeInterface, + SessionClaim, + SessionClaimValidator, + SessionInformation, + TokenTransferMethod, + TypeNormalisedInput, + VerifySessionOptions, +} from './types' +import { availableTokenTransferMethods } from './constants' +import { ParsedJWTInfo, parseJWTWithoutSignatureVerification } from './jwt' +import { validateAccessTokenStructure } from './accessToken' + +export class HandshakeInfo { + constructor( + public antiCsrf: AntiCsrfType, + public accessTokenBlacklistingEnabled: boolean, + public accessTokenValidity: number, + public refreshTokenValidity: number, + private rawJwtSigningPublicKeyList: KeyInfo[], + ) {} + + setJwtSigningPublicKeyList(updatedList: KeyInfo[]) { + this.rawJwtSigningPublicKeyList = updatedList + } + + getJwtSigningPublicKeyList() { + return this.rawJwtSigningPublicKeyList.filter(key => key.expiryTime > Date.now()) + } + + clone() { + return new HandshakeInfo( + this.antiCsrf, + this.accessTokenBlacklistingEnabled, + this.accessTokenValidity, + this.refreshTokenValidity, + this.rawJwtSigningPublicKeyList, + ) + } +} + +export interface Helpers { + querier: Querier + getHandshakeInfo: (forceRefetch?: boolean) => Promise + updateJwtSigningPublicKeyInfo: (keyList: KeyInfo[] | undefined, publicKey: string, expiryTime: number) => void + config: TypeNormalisedInput + appInfo: NormalisedAppinfo + getRecipeImpl: () => RecipeInterface +} + +// We are defining this here to reduce the scope of legacy code +const LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = 'sIdRefreshToken' + +export default function getRecipeInterface( + querier: Querier, + config: TypeNormalisedInput, + appInfo: NormalisedAppinfo, + getRecipeImplAfterOverrides: () => RecipeInterface, +): RecipeInterface { + const helpers: Helpers = { + querier, + updateJwtSigningPublicKeyInfo, + getHandshakeInfo, + config, + appInfo, + getRecipeImpl: getRecipeImplAfterOverrides, + } + + let handshakeInfo: undefined | HandshakeInfo + + async function getHandshakeInfo(forceRefetch = false): Promise { + if (handshakeInfo === undefined || handshakeInfo.getJwtSigningPublicKeyList().length === 0 || forceRefetch) { + const antiCsrf = config.antiCsrf + ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO) + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/handshake'), {}) + + handshakeInfo = new HandshakeInfo( + antiCsrf, + response.accessTokenBlacklistingEnabled, + response.accessTokenValidity, + response.refreshTokenValidity, + response.jwtSigningPublicKeyList, + ) + + updateJwtSigningPublicKeyInfo( + response.jwtSigningPublicKeyList, + response.jwtSigningPublicKey, + response.jwtSigningPublicKeyExpiryTime, + ) + } + return handshakeInfo + } + + function updateJwtSigningPublicKeyInfo(keyList: KeyInfo[] | undefined, publicKey: string, expiryTime: number) { + if (keyList === undefined) { + // Setting createdAt to Date.now() emulates the old lastUpdatedAt logic + keyList = [{ publicKey, expiryTime, createdAt: Date.now() }] + } + + if (handshakeInfo !== undefined) + handshakeInfo.setJwtSigningPublicKeyList(keyList) + } + + const obj: RecipeInterface = { + async createNewSession({ + req, + res, + userId, + accessTokenPayload = {}, + sessionData = {}, + userContext, + }: { + req: BaseRequest + res: BaseResponse + userId: string + accessTokenPayload?: any + sessionData?: any + userContext: any + }): Promise { + logDebugMessage('createNewSession: Started') + let outputTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: true, userContext }) + if (outputTransferMethod === 'any') + outputTransferMethod = 'header' + + logDebugMessage(`createNewSession: using transfer method ${outputTransferMethod}`) + + if ( + outputTransferMethod === 'cookie' + && helpers.config.cookieSameSite === 'none' + && !helpers.config.cookieSecure + && !( + (helpers.appInfo.topLevelAPIDomain === 'localhost' + || isAnIpAddress(helpers.appInfo.topLevelAPIDomain)) + && (helpers.appInfo.topLevelWebsiteDomain === 'localhost' + || isAnIpAddress(helpers.appInfo.topLevelWebsiteDomain)) + ) + ) { + // We can allow insecure cookie when both website & API domain are localhost or an IP + // When either of them is a different domain, API domain needs to have https and a secure cookie to work + throw new Error( + 'Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false.', + ) + } + + const disableAntiCSRF = outputTransferMethod === 'header' + + const response = await SessionFunctions.createNewSession( + helpers, + userId, + disableAntiCSRF, + accessTokenPayload, + sessionData, + ) + + for (const transferMethod of availableTokenTransferMethods) { + if (transferMethod !== outputTransferMethod && getToken(req, 'access', transferMethod) !== undefined) + clearSession(config, res, transferMethod) + } + + attachTokensToResponse(config, res, response, outputTransferMethod) + return new Session( + helpers, + response.accessToken.token, + response.session.handle, + response.session.userId, + response.session.userDataInJWT, + res, + req, + outputTransferMethod, + ) + }, + + async getGlobalClaimValidators(input: { + claimValidatorsAddedByOtherRecipes: SessionClaimValidator[] + }) { + return input.claimValidatorsAddedByOtherRecipes + }, + + /* In all cases if sIdRefreshToken token exists (so it's a legacy session) we return TRY_REFRESH_TOKEN. The refresh endpoint will clear this cookie and try to upgrade the session. + Check https://supertokens.com/docs/contribute/decisions/session/0007 for further details and a table of expected behaviours + */ + async getSession({ + req, + res, + options, + userContext, + }: { + req: BaseRequest + res: BaseResponse + options?: VerifySessionOptions + userContext: any + }): Promise { + logDebugMessage('getSession: Started') + + // This token isn't handled by getToken to limit the scope of this legacy/migration code + if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { + // This could create a spike on refresh calls during the update of the backend SDK + throw new STError({ + message: 'using legacy session, please call the refresh API', + type: STError.TRY_REFRESH_TOKEN, + }) + } + + const sessionOptional = options?.sessionRequired === false + logDebugMessage(`getSession: optional validation: ${sessionOptional}`) + + const accessTokens: { + [key in TokenTransferMethod]?: ParsedJWTInfo; + } = {} + + // We check all token transfer methods for available access tokens + for (const transferMethod of availableTokenTransferMethods) { + const tokenString = getToken(req, 'access', transferMethod) + if (tokenString !== undefined) { + try { + const info = parseJWTWithoutSignatureVerification(tokenString) + validateAccessTokenStructure(info.payload) + logDebugMessage(`getSession: got access token from ${transferMethod}`) + accessTokens[transferMethod] = info + } + catch { + logDebugMessage( + `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure`, + ) + } + } + } + + const allowedTransferMethod = config.getTokenTransferMethod({ + req, + forCreateNewSession: false, + userContext, + }) + let requestTransferMethod: TokenTransferMethod + let accessToken: ParsedJWTInfo | undefined + + if ( + (allowedTransferMethod === 'any' || allowedTransferMethod === 'header') + && accessTokens.header !== undefined + ) { + logDebugMessage('getSession: using header transfer method') + requestTransferMethod = 'header' + accessToken = accessTokens.header + } + else if ( + (allowedTransferMethod === 'any' || allowedTransferMethod === 'cookie') + && accessTokens.cookie !== undefined + ) { + logDebugMessage('getSession: using cookie transfer method') + requestTransferMethod = 'cookie' + accessToken = accessTokens.cookie + } + else { + if (sessionOptional) { + logDebugMessage( + 'getSession: returning undefined because accessToken is undefined and sessionRequired is false', + ) + // there is no session that exists here, and the user wants session verification + // to be optional. So we return undefined. + return undefined + } + + logDebugMessage('getSession: UNAUTHORISED because accessToken in request is undefined') + throw new STError({ + message: + 'Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?', + type: STError.UNAUTHORISED, + payload: { + // we do not clear the session here because of a + // race condition mentioned here: https://github.com/supertokens/supertokens-node/issues/17 + clearTokens: false, + }, + }) + } + + const antiCsrfToken = getAntiCsrfTokenFromHeaders(req) + let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined + + if (doAntiCsrfCheck === undefined) + doAntiCsrfCheck = normaliseHttpMethod(req.getMethod()) !== 'get' + + if (requestTransferMethod === 'header') + doAntiCsrfCheck = false + + logDebugMessage(`getSession: Value of doAntiCsrfCheck is: ${doAntiCsrfCheck}`) + + const response = await SessionFunctions.getSession( + helpers, + accessToken, + antiCsrfToken, + doAntiCsrfCheck, + getRidFromHeader(req) !== undefined, + ) + let accessTokenString = accessToken.rawTokenString + if (response.accessToken !== undefined) { + setFrontTokenInHeaders( + res, + response.session.userId, + response.accessToken.expiry, + response.session.userDataInJWT, + ) + setToken( + config, + res, + 'access', + response.accessToken.token, + // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. + // Even if the token is expired the presence of the token indicates that the user could have a valid refresh + // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + Date.now() + 3153600000000, + requestTransferMethod, + ) + accessTokenString = response.accessToken.token + } + logDebugMessage('getSession: Success!') + const session = new Session( + helpers, + accessTokenString, + response.session.handle, + response.session.userId, + response.session.userDataInJWT, + res, + req, + requestTransferMethod, + ) + + return session + }, + + async validateClaims( + this: RecipeInterface, + input: { + userId: string + accessTokenPayload: any + claimValidators: SessionClaimValidator[] + userContext: any + }, + ): Promise<{ + invalidClaims: ClaimValidationError[] + accessTokenPayloadUpdate?: any + }> { + let accessTokenPayload = input.accessTokenPayload + let accessTokenPayloadUpdate + const origSessionClaimPayloadJSON = JSON.stringify(accessTokenPayload) + + for (const validator of input.claimValidators) { + logDebugMessage(`updateClaimsInPayloadIfNeeded checking shouldRefetch for ${validator.id}`) + if ('claim' in validator && (await validator.shouldRefetch(accessTokenPayload, input.userContext))) { + logDebugMessage(`updateClaimsInPayloadIfNeeded refetching ${validator.id}`) + const value = await validator.claim.fetchValue(input.userId, input.userContext) + logDebugMessage( + `updateClaimsInPayloadIfNeeded ${validator.id} refetch result ${JSON.stringify(value)}`, + ) + if (value !== undefined) { + accessTokenPayload = validator.claim.addToPayload_internal( + accessTokenPayload, + value, + input.userContext, + ) + } + } + } + + if (JSON.stringify(accessTokenPayload) !== origSessionClaimPayloadJSON) + accessTokenPayloadUpdate = accessTokenPayload + + const invalidClaims = await validateClaimsInPayload( + input.claimValidators, + accessTokenPayload, + input.userContext, + ) + + return { + invalidClaims, + accessTokenPayloadUpdate, + } + }, + + async validateClaimsInJWTPayload( + this: RecipeInterface, + input: { + userId: string + jwtPayload: JSONObject + claimValidators: SessionClaimValidator[] + userContext: any + }, + ): Promise<{ + status: 'OK' + invalidClaims: ClaimValidationError[] + }> { + // We skip refetching here, because we have no way of updating the JWT payload here + // if we have access to the entire session other methods can be used to do validation while updating + const invalidClaims = await validateClaimsInPayload( + input.claimValidators, + input.jwtPayload, + input.userContext, + ) + + return { + status: 'OK', + invalidClaims, + } + }, + + async getSessionInformation({ + sessionHandle, + }: { + sessionHandle: string + }): Promise { + return SessionFunctions.getSessionInformation(helpers, sessionHandle) + }, + + /* + In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. + Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours + */ + async refreshSession( + this: RecipeInterface, + { req, res, userContext }: { req: BaseRequest; res: BaseResponse; userContext: any }, + ): Promise { + logDebugMessage('refreshSession: Started') + + const refreshTokens: { + [key in TokenTransferMethod]?: string; + } = {} + + // We check all token transfer methods for available refresh tokens + // We do this so that we can later clear all we are not overwriting + for (const transferMethod of availableTokenTransferMethods) { + refreshTokens[transferMethod] = getToken(req, 'refresh', transferMethod) + if (refreshTokens[transferMethod] !== undefined) + logDebugMessage(`refreshSession: got refresh token from ${transferMethod}`) + } + + const allowedTransferMethod = config.getTokenTransferMethod({ + req, + forCreateNewSession: false, + userContext, + }) + logDebugMessage(`refreshSession: getTokenTransferMethod returned ${allowedTransferMethod}`) + + let requestTransferMethod: TokenTransferMethod + let refreshToken: string | undefined + + if ( + (allowedTransferMethod === 'any' || allowedTransferMethod === 'header') + && refreshTokens.header !== undefined + ) { + logDebugMessage('refreshSession: using header transfer method') + requestTransferMethod = 'header' + refreshToken = refreshTokens.header + } + else if ( + (allowedTransferMethod === 'any' || allowedTransferMethod === 'cookie') + && refreshTokens.cookie + ) { + logDebugMessage('refreshSession: using cookie transfer method') + requestTransferMethod = 'cookie' + refreshToken = refreshTokens.cookie + } + else { + // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code + if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { + logDebugMessage( + 'refreshSession: cleared legacy id refresh token because refresh token was not found', + ) + setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, '', 0, 'accessTokenPath') + } + + logDebugMessage('refreshSession: UNAUTHORISED because refresh token in request is undefined') + throw new STError({ + message: 'Refresh token not found. Are you sending the refresh token in the request?', + payload: { + clearTokens: false, + }, + type: STError.UNAUTHORISED, + }) + } + + try { + const antiCsrfToken = getAntiCsrfTokenFromHeaders(req) + const response = await SessionFunctions.refreshSession( + helpers, + refreshToken, + antiCsrfToken, + getRidFromHeader(req) !== undefined, + requestTransferMethod, + ) + logDebugMessage(`refreshSession: Attaching refreshed session info as ${requestTransferMethod}`) + + // We clear the tokens in all token transfer methods we are not going to overwrite + for (const transferMethod of availableTokenTransferMethods) { + if (transferMethod !== requestTransferMethod && refreshTokens[transferMethod] !== undefined) + clearSession(config, res, transferMethod) + } + + attachTokensToResponse(config, res, response, requestTransferMethod) + + logDebugMessage('refreshSession: Success!') + // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code + if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { + logDebugMessage('refreshSession: cleared legacy id refresh token after successful refresh') + setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, '', 0, 'accessTokenPath') + } + + return new Session( + helpers, + response.accessToken.token, + response.session.handle, + response.session.userId, + response.session.userDataInJWT, + res, + req, + requestTransferMethod, + ) + } + catch (err: any) { + if (err.type === STError.TOKEN_THEFT_DETECTED || err.payload.clearTokens) { + // This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code + if (req.getCookieValue(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) !== undefined) { + logDebugMessage( + 'refreshSession: cleared legacy id refresh token because refresh is clearing other tokens', + ) + setCookie(config, res, LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, '', 0, 'accessTokenPath') + } + } + throw err + } + }, + + async regenerateAccessToken( + this: RecipeInterface, + input: { + accessToken: string + newAccessTokenPayload?: any + userContext: any + }, + ): Promise< + | { + status: 'OK' + session: { + handle: string + userId: string + userDataInJWT: any + } + accessToken?: { + token: string + expiry: number + createdTime: number + } + } + | undefined + > { + const newAccessTokenPayload + = (input.newAccessTokenPayload === null || input.newAccessTokenPayload === undefined) + ? {} + : input.newAccessTokenPayload + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/session/regenerate'), { + accessToken: input.accessToken, + userDataInJWT: newAccessTokenPayload, + }) + if (response.status === 'UNAUTHORISED') + return undefined + + return response + }, + + revokeAllSessionsForUser({ userId }: { userId: string }) { + return SessionFunctions.revokeAllSessionsForUser(helpers, userId) + }, + + getAllSessionHandlesForUser({ userId }: { userId: string }): Promise { + return SessionFunctions.getAllSessionHandlesForUser(helpers, userId) + }, + + revokeSession({ sessionHandle }: { sessionHandle: string }): Promise { + return SessionFunctions.revokeSession(helpers, sessionHandle) + }, + + revokeMultipleSessions({ sessionHandles }: { sessionHandles: string[] }) { + return SessionFunctions.revokeMultipleSessions(helpers, sessionHandles) + }, + + updateSessionData({ + sessionHandle, + newSessionData, + }: { + sessionHandle: string + newSessionData: any + }): Promise { + return SessionFunctions.updateSessionData(helpers, sessionHandle, newSessionData) + }, + + updateAccessTokenPayload({ + sessionHandle, + newAccessTokenPayload, + }: { + sessionHandle: string + newAccessTokenPayload: any + }): Promise { + return SessionFunctions.updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPayload) + }, + + async mergeIntoAccessTokenPayload( + this: RecipeInterface, + { + sessionHandle, + accessTokenPayloadUpdate, + userContext, + }: { + sessionHandle: string + accessTokenPayloadUpdate: JSONObject + userContext: any + }, + ) { + const sessionInfo = await this.getSessionInformation({ sessionHandle, userContext }) + if (sessionInfo === undefined) + return false + + const newAccessTokenPayload = { ...sessionInfo.accessTokenPayload, ...accessTokenPayloadUpdate } + for (const key of Object.keys(accessTokenPayloadUpdate)) { + if (accessTokenPayloadUpdate[key] === null) + delete newAccessTokenPayload[key] + } + return this.updateAccessTokenPayload({ sessionHandle, newAccessTokenPayload, userContext }) + }, + + async getAccessTokenLifeTimeMS(): Promise { + return (await getHandshakeInfo()).accessTokenValidity + }, + + async getRefreshTokenLifeTimeMS(): Promise { + return (await getHandshakeInfo()).refreshTokenValidity + }, + + async fetchAndSetClaim( + this: RecipeInterface, + input: { + sessionHandle: string + claim: SessionClaim + userContext?: any + }, + ) { + const sessionInfo = await this.getSessionInformation({ + sessionHandle: input.sessionHandle, + userContext: input.userContext, + }) + if (sessionInfo === undefined) + return false + + const accessTokenPayloadUpdate = await input.claim.build(sessionInfo.userId, input.userContext) + + return this.mergeIntoAccessTokenPayload({ + sessionHandle: input.sessionHandle, + accessTokenPayloadUpdate, + userContext: input.userContext, + }) + }, + + setClaimValue( + this: RecipeInterface, + input: { + sessionHandle: string + claim: SessionClaim + value: T + userContext?: any + }, + ) { + const accessTokenPayloadUpdate = input.claim.addToPayload_internal({}, input.value, input.userContext) + return this.mergeIntoAccessTokenPayload({ + sessionHandle: input.sessionHandle, + accessTokenPayloadUpdate, + userContext: input.userContext, + }) + }, + + async getClaimValue( + this: RecipeInterface, + input: { sessionHandle: string; claim: SessionClaim; userContext?: any }, + ) { + const sessionInfo = await this.getSessionInformation({ + sessionHandle: input.sessionHandle, + userContext: input.userContext, + }) + + if (sessionInfo === undefined) { + return { + status: 'SESSION_DOES_NOT_EXIST_ERROR', + } + } + + return { + status: 'OK', + value: input.claim.getValueFromPayload(sessionInfo.accessTokenPayload, input.userContext), + } + }, + + removeClaim( + this: RecipeInterface, + input: { sessionHandle: string; claim: SessionClaim; userContext?: any }, + ) { + const accessTokenPayloadUpdate = input.claim.removeFromPayloadByMerge_internal({}, input.userContext) + + return this.mergeIntoAccessTokenPayload({ + sessionHandle: input.sessionHandle, + accessTokenPayloadUpdate, + userContext: input.userContext, + }) + }, + } + + if (process.env.TEST_MODE === 'testing') { + // testing mode, we add some of the help functions to the obj + (obj as any).getHandshakeInfo = getHandshakeInfo; + (obj as any).updateJwtSigningPublicKeyInfo = updateJwtSigningPublicKeyInfo; + (obj as any).helpers = helpers; + (obj as any).setHandshakeInfo = function (info: any) { + handshakeInfo = info + } + } + + return obj +} diff --git a/src/recipe/session/sessionClass.ts b/src/recipe/session/sessionClass.ts new file mode 100644 index 000000000..aa592de79 --- /dev/null +++ b/src/recipe/session/sessionClass.ts @@ -0,0 +1,220 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { clearSession, setFrontTokenInHeaders, setToken } from './cookieAndHeaders' +import STError from './error' +import { SessionClaim, SessionClaimValidator, SessionContainerInterface, TokenTransferMethod } from './types' +import { Helpers } from './recipeImplementation' + +export default class Session implements SessionContainerInterface { + constructor( + protected helpers: Helpers, + protected accessToken: string, + protected sessionHandle: string, + protected userId: string, + protected userDataInAccessToken: any, + protected res: BaseResponse, + protected readonly req: BaseRequest, + protected readonly transferMethod: TokenTransferMethod, + ) {} + + async revokeSession(userContext?: any) { + await this.helpers.getRecipeImpl().revokeSession({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, + }) + + // we do not check the output of calling revokeSession + // before clearing the cookies because we are revoking the + // current API request's session. + // If we instead clear the cookies only when revokeSession + // returns true, it can cause this kind of a bug: + // https://github.com/supertokens/supertokens-node/issues/343 + clearSession(this.helpers.config, this.res, this.transferMethod) + } + + async getSessionData(userContext?: any): Promise { + const sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, + }) + if (sessionInfo === undefined) { + throw new STError({ + message: 'Session does not exist anymore', + type: STError.UNAUTHORISED, + }) + } + return sessionInfo.sessionData + } + + async updateSessionData(newSessionData: any, userContext?: any) { + if ( + !(await this.helpers.getRecipeImpl().updateSessionData({ + sessionHandle: this.sessionHandle, + newSessionData, + userContext: userContext === undefined ? {} : userContext, + })) + ) { + throw new STError({ + message: 'Session does not exist anymore', + type: STError.UNAUTHORISED, + }) + } + } + + getUserId(_userContext?: any) { + return this.userId + } + + getAccessTokenPayload(_userContext?: any) { + return this.userDataInAccessToken + } + + getHandle() { + return this.sessionHandle + } + + getAccessToken() { + return this.accessToken + } + + // Any update to this function should also be reflected in the respective JWT version + async mergeIntoAccessTokenPayload(accessTokenPayloadUpdate?: any, userContext?: any): Promise { + const updatedPayload = { ...this.getAccessTokenPayload(userContext), ...accessTokenPayloadUpdate } + + if (accessTokenPayloadUpdate) { + for (const key of Object.keys(accessTokenPayloadUpdate)) { + if (accessTokenPayloadUpdate[key] === null) + delete updatedPayload[key] + } + await this.updateAccessTokenPayload(updatedPayload, userContext) + } + + if (accessTokenPayloadUpdate === undefined) + await this.updateAccessTokenPayload(undefined, undefined) + } + + async getTimeCreated(userContext?: any): Promise { + const sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, + }) + if (sessionInfo === undefined) { + throw new STError({ + message: 'Session does not exist anymore', + type: STError.UNAUTHORISED, + }) + } + return sessionInfo.timeCreated + } + + async getExpiry(userContext?: any): Promise { + const sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ + sessionHandle: this.sessionHandle, + userContext: userContext === undefined ? {} : userContext, + }) + if (sessionInfo === undefined) { + throw new STError({ + message: 'Session does not exist anymore', + type: STError.UNAUTHORISED, + }) + } + return sessionInfo.expiry + } + + // Any update to this function should also be reflected in the respective JWT version + async assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise { + const validateClaimResponse = await this.helpers.getRecipeImpl().validateClaims({ + accessTokenPayload: this.getAccessTokenPayload(userContext), + userId: this.getUserId(userContext), + claimValidators, + userContext, + }) + + if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) + await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext) + + if (validateClaimResponse.invalidClaims.length !== 0) { + throw new STError({ + type: 'INVALID_CLAIMS', + message: 'INVALID_CLAIMS', + payload: validateClaimResponse.invalidClaims, + }) + } + } + + // Any update to this function should also be reflected in the respective JWT version + async fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise { + const update = await claim.build(this.getUserId(userContext), userContext) + return this.mergeIntoAccessTokenPayload(update, userContext) + } + + // Any update to this function should also be reflected in the respective JWT version + setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise { + const update = claim.addToPayload_internal({}, value, userContext) + return this.mergeIntoAccessTokenPayload(update, userContext) + } + + // Any update to this function should also be reflected in the respective JWT version + async getClaimValue(claim: SessionClaim, userContext?: any) { + return claim.getValueFromPayload(await this.getAccessTokenPayload(userContext), userContext) + } + + // Any update to this function should also be reflected in the respective JWT version + removeClaim(claim: SessionClaim, userContext?: any): Promise { + const update = claim.removeFromPayloadByMerge_internal({}, userContext) + return this.mergeIntoAccessTokenPayload(update, userContext) + } + + /** + * @deprecated Use mergeIntoAccessTokenPayload + */ + async updateAccessTokenPayload(newAccessTokenPayload?: any, userContext?: any): Promise { + const response = await this.helpers.getRecipeImpl().regenerateAccessToken({ + accessToken: this.getAccessToken(), + newAccessTokenPayload, + userContext: userContext === undefined ? {} : userContext, + }) + if (response === undefined) { + throw new STError({ + message: 'Session does not exist anymore', + type: STError.UNAUTHORISED, + }) + } + this.userDataInAccessToken = response.session.userDataInJWT + if (response.accessToken !== undefined) { + this.accessToken = response.accessToken.token + setFrontTokenInHeaders( + this.res, + response.session.userId, + response.accessToken.expiry, + response.session.userDataInJWT, + ) + setToken( + this.helpers.config, + this.res, + 'access', + response.accessToken.token, + // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. + // Even if the token is expired the presence of the token indicates that the user could have a valid refresh + // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + Date.now() + 3153600000000, + this.transferMethod, + ) + } + } +} diff --git a/src/recipe/session/sessionFunctions.ts b/src/recipe/session/sessionFunctions.ts new file mode 100644 index 000000000..832df9158 --- /dev/null +++ b/src/recipe/session/sessionFunctions.ts @@ -0,0 +1,443 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { PROCESS_STATE, ProcessState } from '../../processState' +import NormalisedURLPath from '../../normalisedURLPath' +import { maxVersion } from '../../utils' +import { logDebugMessage } from '../../logger' +import { getInfoFromAccessToken, sanitizeNumberInput } from './accessToken' +import { ParsedJWTInfo } from './jwt' +import STError from './error' +import { CreateOrRefreshAPIResponse, SessionInformation, TokenTransferMethod } from './types' +import { Helpers } from './recipeImplementation' + +/** + * @description call this to "login" a user. + */ +export async function createNewSession( + helpers: Helpers, + userId: string, + disableAntiCsrf: boolean, + accessTokenPayload: any = {}, + sessionData: any = {}, +): Promise { + accessTokenPayload = (accessTokenPayload === null || accessTokenPayload === undefined) ? {} : accessTokenPayload + sessionData = (sessionData === null || sessionData === undefined) ? {} : sessionData + + const requestBody: { + userId: string + userDataInJWT: any + userDataInDatabase: any + enableAntiCsrf?: boolean + } = { + userId, + userDataInJWT: accessTokenPayload, + userDataInDatabase: sessionData, + } + + const handShakeInfo = await helpers.getHandshakeInfo() + requestBody.enableAntiCsrf = !disableAntiCsrf && handShakeInfo.antiCsrf === 'VIA_TOKEN' + const response = await helpers.querier.sendPostRequest(new NormalisedURLPath('/recipe/session'), requestBody) + helpers.updateJwtSigningPublicKeyInfo( + response.jwtSigningPublicKeyList, + response.jwtSigningPublicKey, + response.jwtSigningPublicKeyExpiryTime, + ) + delete response.status + delete response.jwtSigningPublicKey + delete response.jwtSigningPublicKeyExpiryTime + delete response.jwtSigningPublicKeyList + + return response +} + +/** + * @description authenticates a session. To be used in APIs that require authentication + */ +export async function getSession( + helpers: Helpers, + parsedAccessToken: ParsedJWTInfo, + antiCsrfToken: string | undefined, + doAntiCsrfCheck: boolean, + containsCustomHeader: boolean, +): Promise<{ + session: { + handle: string + userId: string + userDataInJWT: any + } + accessToken?: { + token: string + expiry: number + createdTime: number + } +}> { + const handShakeInfo = await helpers.getHandshakeInfo() + let accessTokenInfo + + // If we have no key old enough to verify this access token we should reject it without calling the core + let foundASigningKeyThatIsOlderThanTheAccessToken = false + for (const key of handShakeInfo.getJwtSigningPublicKeyList()) { + try { + /** + * get access token info using existing signingKey + */ + accessTokenInfo = await getInfoFromAccessToken( + parsedAccessToken, + key.publicKey, + handShakeInfo.antiCsrf === 'VIA_TOKEN' && doAntiCsrfCheck, + ) + foundASigningKeyThatIsOlderThanTheAccessToken = true + } + catch (err: any) { + /** + * if error type is not TRY_REFRESH_TOKEN, we return the + * error to the user + */ + if (err.type !== STError.TRY_REFRESH_TOKEN) + throw err + + /** + * if it comes here, it means token verification has failed. + * It may be due to: + * - signing key was updated and this token was signed with new key + * - access token is actually expired + * - access token was signed with the older signing key + * + * if access token is actually expired, we don't need to call core and + * just return TRY_REFRESH_TOKEN to the client + * + * if access token creation time is after this signing key was created + * we need to call core as there are chances that the token + * was signed with the updated signing key + * + * if access token creation time is before oldest signing key was created, + * so if foundASigningKeyThatIsOlderThanTheAccessToken is still false after + * the loop we just return TRY_REFRESH_TOKEN + */ + const payload = parsedAccessToken.payload + + const timeCreated = sanitizeNumberInput(payload.timeCreated) + const expiryTime = sanitizeNumberInput(payload.expiryTime) + + if (expiryTime === undefined || expiryTime < Date.now()) + throw err + + if (timeCreated === undefined) + throw err + + // If we reached a key older than the token and failed to validate the token, + // that means it was signed by a key newer than the cached list. + // In this case we go to the server. + if (timeCreated >= key.createdAt) { + foundASigningKeyThatIsOlderThanTheAccessToken = true + break + } + } + } + + // If the token was created before the oldest key in the cache but hasn't expired, then a config value must've changed. + // E.g., the access_token_signing_key_update_interval was reduced, or access_token_signing_key_dynamic was turned on. + // Either way, the user needs to refresh the access token as validating by the server is likely to do nothing. + if (!foundASigningKeyThatIsOlderThanTheAccessToken) { + throw new STError({ + message: 'Access token has expired. Please call the refresh API', + type: STError.TRY_REFRESH_TOKEN, + }) + } + + /** + * anti-csrf check if accesstokenInfo is not undefined, + * which means token verification was successful + */ + if (doAntiCsrfCheck) { + if (handShakeInfo.antiCsrf === 'VIA_TOKEN') { + if (accessTokenInfo !== undefined) { + if (antiCsrfToken === undefined || antiCsrfToken !== accessTokenInfo.antiCsrfToken) { + if (antiCsrfToken === undefined) { + logDebugMessage( + 'getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request', + ) + throw new STError({ + message: + 'Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API', + type: STError.TRY_REFRESH_TOKEN, + }) + } + else { + logDebugMessage( + 'getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token', + ) + throw new STError({ + message: 'anti-csrf check failed', + type: STError.TRY_REFRESH_TOKEN, + }) + } + } + } + } + else if (handShakeInfo.antiCsrf === 'VIA_CUSTOM_HEADER') { + if (!containsCustomHeader) { + logDebugMessage('getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed') + throw new STError({ + message: + 'anti-csrf check failed. Please pass \'rid: "session"\' header in the request, or set doAntiCsrfCheck to false for this API', + type: STError.TRY_REFRESH_TOKEN, + }) + } + } + } + if ( + accessTokenInfo !== undefined + && !handShakeInfo.accessTokenBlacklistingEnabled + && accessTokenInfo.parentRefreshTokenHash1 === undefined + ) { + return { + session: { + handle: accessTokenInfo.sessionHandle, + userId: accessTokenInfo.userId, + userDataInJWT: accessTokenInfo.userData, + }, + } + } + + ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + + const requestBody: { + accessToken: string + antiCsrfToken?: string + doAntiCsrfCheck: boolean + enableAntiCsrf?: boolean + } = { + accessToken: parsedAccessToken.rawTokenString, + antiCsrfToken, + doAntiCsrfCheck, + enableAntiCsrf: handShakeInfo.antiCsrf === 'VIA_TOKEN', + } + + const response = await helpers.querier.sendPostRequest(new NormalisedURLPath('/recipe/session/verify'), requestBody) + if (response.status === 'OK') { + helpers.updateJwtSigningPublicKeyInfo( + response.jwtSigningPublicKeyList, + response.jwtSigningPublicKey, + response.jwtSigningPublicKeyExpiryTime, + ) + delete response.status + delete response.jwtSigningPublicKey + delete response.jwtSigningPublicKeyExpiryTime + delete response.jwtSigningPublicKeyList + return response + } + else if (response.status === 'UNAUTHORISED') { + logDebugMessage('getSession: Returning UNAUTHORISED because of core response') + throw new STError({ + message: response.message, + type: STError.UNAUTHORISED, + }) + } + else { + if ( + response.jwtSigningPublicKeyList !== undefined + || (response.jwtSigningPublicKey !== undefined && response.jwtSigningPublicKeyExpiryTime !== undefined) + ) { + // after CDI 2.7.1, the API returns the new keys + helpers.updateJwtSigningPublicKeyInfo( + response.jwtSigningPublicKeyList, + response.jwtSigningPublicKey, + response.jwtSigningPublicKeyExpiryTime, + ) + } + else { + // we force update the signing keys... + await helpers.getHandshakeInfo(true) + } + logDebugMessage('getSession: Returning TRY_REFRESH_TOKEN because of core response.') + throw new STError({ + message: response.message, + type: STError.TRY_REFRESH_TOKEN, + }) + } +} + +/** + * @description Retrieves session information from storage for a given session handle + * @returns session data stored in the database, including userData and access token payload, or undefined if sessionHandle is invalid + */ +export async function getSessionInformation( + helpers: Helpers, + sessionHandle: string, +): Promise { + const apiVersion = await helpers.querier.getAPIVersion() + + if (maxVersion(apiVersion, '2.7') === '2.7') + throw new Error('Please use core version >= 3.5 to call this function.') + + const response = await helpers.querier.sendGetRequest(new NormalisedURLPath('/recipe/session'), { + sessionHandle, + }) + + if (response.status === 'OK') { + // Change keys to make them more readable + response.sessionData = response.userDataInDatabase + response.accessTokenPayload = response.userDataInJWT + + delete response.userDataInJWT + delete response.userDataInJWT + + return response + } + else { + return undefined + } +} + +/** + * @description generates new access and refresh tokens for a given refresh token. Called when client's access token has expired. + * @sideEffects calls onTokenTheftDetection if token theft is detected. + */ +export async function refreshSession( + helpers: Helpers, + refreshToken: string, + antiCsrfToken: string | undefined, + containsCustomHeader: boolean, + transferMethod: TokenTransferMethod, +): Promise { + const handShakeInfo = await helpers.getHandshakeInfo() + + const requestBody: { + refreshToken: string + antiCsrfToken?: string + enableAntiCsrf?: boolean + } = { + refreshToken, + antiCsrfToken, + enableAntiCsrf: transferMethod === 'cookie' && handShakeInfo.antiCsrf === 'VIA_TOKEN', + } + + if (handShakeInfo.antiCsrf === 'VIA_CUSTOM_HEADER' && transferMethod === 'cookie') { + if (!containsCustomHeader) { + logDebugMessage('refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed') + throw new STError({ + message: 'anti-csrf check failed. Please pass \'rid: "session"\' header in the request.', + type: STError.UNAUTHORISED, + payload: { + clearTokens: false, // see https://github.com/supertokens/supertokens-node/issues/141 + }, + }) + } + } + + const response = await helpers.querier.sendPostRequest(new NormalisedURLPath('/recipe/session/refresh'), requestBody) + if (response.status === 'OK') { + delete response.status + return response + } + else if (response.status === 'UNAUTHORISED') { + logDebugMessage('refreshSession: Returning UNAUTHORISED because of core response') + throw new STError({ + message: response.message, + type: STError.UNAUTHORISED, + }) + } + else { + logDebugMessage('refreshSession: Returning TOKEN_THEFT_DETECTED because of core response') + throw new STError({ + message: 'Token theft detected', + payload: { + userId: response.session.userId, + sessionHandle: response.session.handle, + }, + type: STError.TOKEN_THEFT_DETECTED, + }) + } +} + +/** + * @description deletes session info of a user from db. This only invalidates the refresh token. Not the access token. + * Access tokens cannot be immediately invalidated. Unless we add a blacklisting method. Or changed the private key to sign them. + */ +export async function revokeAllSessionsForUser(helpers: Helpers, userId: string): Promise { + const response = await helpers.querier.sendPostRequest(new NormalisedURLPath('/recipe/session/remove'), { + userId, + }) + return response.sessionHandlesRevoked +} + +/** + * @description gets all session handles for current user. Please do not call this unless this user is authenticated. + */ +export async function getAllSessionHandlesForUser(helpers: Helpers, userId: string): Promise { + const response = await helpers.querier.sendGetRequest(new NormalisedURLPath('/recipe/session/user'), { + userId, + }) + return response.sessionHandles +} + +/** + * @description call to destroy one session + * @returns true if session was deleted from db. Else false in case there was nothing to delete + */ +export async function revokeSession(helpers: Helpers, sessionHandle: string): Promise { + const response = await helpers.querier.sendPostRequest(new NormalisedURLPath('/recipe/session/remove'), { + sessionHandles: [sessionHandle], + }) + return response.sessionHandlesRevoked.length === 1 +} + +/** + * @description call to destroy multiple sessions + * @returns list of sessions revoked + */ +export async function revokeMultipleSessions(helpers: Helpers, sessionHandles: string[]): Promise { + const response = await helpers.querier.sendPostRequest(new NormalisedURLPath('/recipe/session/remove'), { + sessionHandles, + }) + return response.sessionHandlesRevoked +} + +/** + * @description: It provides no locking mechanism in case other processes are updating session data for this session as well. + */ +export async function updateSessionData( + helpers: Helpers, + sessionHandle: string, + newSessionData: any, +): Promise { + newSessionData = (newSessionData === null || newSessionData === undefined) ? {} : newSessionData + const response = await helpers.querier.sendPutRequest(new NormalisedURLPath('/recipe/session/data'), { + sessionHandle, + userDataInDatabase: newSessionData, + }) + if (response.status === 'UNAUTHORISED') + return false + + return true +} + +export async function updateAccessTokenPayload( + helpers: Helpers, + sessionHandle: string, + newAccessTokenPayload: any, +): Promise { + newAccessTokenPayload + = (newAccessTokenPayload === null || newAccessTokenPayload === undefined) ? {} : newAccessTokenPayload + const response = await helpers.querier.sendPutRequest(new NormalisedURLPath('/recipe/jwt/data'), { + sessionHandle, + userDataInJWT: newAccessTokenPayload, + }) + if (response.status === 'UNAUTHORISED') + return false + + return true +} diff --git a/src/recipe/session/types.ts b/src/recipe/session/types.ts new file mode 100644 index 000000000..d4d2eb02a --- /dev/null +++ b/src/recipe/session/types.ts @@ -0,0 +1,503 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest, BaseResponse } from '../../framework' +import NormalisedURLPath from '../../normalisedURLPath' +import { APIInterface as JWTAPIInterface, RecipeInterface as JWTRecipeInterface } from '../jwt/types' +import { APIInterface as OpenIdAPIInterface, RecipeInterface as OpenIdRecipeInterface } from '../openid/types' +import { GeneralErrorResponse, JSONObject, JSONValue } from '../../types' + +export interface KeyInfo { + publicKey: string + expiryTime: number + createdAt: number +} + +export type AntiCsrfType = 'VIA_TOKEN' | 'VIA_CUSTOM_HEADER' | 'NONE' +export type StoredHandshakeInfo = { + antiCsrf: AntiCsrfType + accessTokenBlacklistingEnabled: boolean + accessTokenValidity: number + refreshTokenValidity: number +} & ( + | { + // Stored after 2.9 + jwtSigningPublicKeyList: KeyInfo[] + } + | { + // Stored before 2.9 + jwtSigningPublicKeyList: undefined + jwtSigningPublicKey: string + jwtSigningPublicKeyExpiryTime: number + } +) + +export interface CreateOrRefreshAPIResponse { + session: { + handle: string + userId: string + userDataInJWT: any + } + accessToken: { + token: string + expiry: number + createdTime: number + } + refreshToken: { + token: string + expiry: number + createdTime: number + } + antiCsrfToken: string | undefined +} + +export interface ErrorHandlers { + onUnauthorised?: ErrorHandlerMiddleware + onTokenTheftDetected?: TokenTheftErrorHandlerMiddleware + onInvalidClaim?: InvalidClaimErrorHandlerMiddleware +} + +export type TokenType = 'access' | 'refresh' + +// When adding a new token transfer method, it's also necessary to update the related constant (availableTokenTransferMethods) +export type TokenTransferMethod = 'header' | 'cookie' + +export interface TypeInput { + sessionExpiredStatusCode?: number + invalidClaimStatusCode?: number + accessTokenPath?: string + cookieSecure?: boolean + cookieSameSite?: 'strict' | 'lax' | 'none' + cookieDomain?: string + + getTokenTransferMethod?: (input: { + req: BaseRequest + forCreateNewSession: boolean + userContext: any + }) => TokenTransferMethod | 'any' + + errorHandlers?: ErrorHandlers + antiCsrf?: 'VIA_TOKEN' | 'VIA_CUSTOM_HEADER' | 'NONE' + jwt?: + | { + enable: true + propertyNameInAccessTokenPayload?: string + issuer?: string + } + | { enable: false } + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + openIdFeature?: { + functions?: ( + originalImplementation: OpenIdRecipeInterface, + builder?: OverrideableBuilder + ) => OpenIdRecipeInterface + apis?: ( + originalImplementation: OpenIdAPIInterface, + builder?: OverrideableBuilder + ) => OpenIdAPIInterface + jwtFeature?: { + functions?: ( + originalImplementation: JWTRecipeInterface, + builder?: OverrideableBuilder + ) => JWTRecipeInterface + apis?: ( + originalImplementation: JWTAPIInterface, + builder?: OverrideableBuilder + ) => JWTAPIInterface + } + } + } +} + +export interface TypeNormalisedInput { + refreshTokenPath: NormalisedURLPath + accessTokenPath: NormalisedURLPath + cookieDomain: string | undefined + cookieSameSite: 'strict' | 'lax' | 'none' + cookieSecure: boolean + sessionExpiredStatusCode: number + errorHandlers: NormalisedErrorHandlers + antiCsrf: 'VIA_TOKEN' | 'VIA_CUSTOM_HEADER' | 'NONE' + + getTokenTransferMethod: (input: { + req: BaseRequest + forCreateNewSession: boolean + userContext: any + }) => TokenTransferMethod | 'any' + + invalidClaimStatusCode: number + jwt: { + enable: boolean + propertyNameInAccessTokenPayload: string + issuer?: string + } + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + openIdFeature?: { + functions?: ( + originalImplementation: OpenIdRecipeInterface, + builder?: OverrideableBuilder + ) => OpenIdRecipeInterface + apis?: ( + originalImplementation: OpenIdAPIInterface, + builder?: OverrideableBuilder + ) => OpenIdAPIInterface + jwtFeature?: { + functions?: ( + originalImplementation: JWTRecipeInterface, + builder?: OverrideableBuilder + ) => JWTRecipeInterface + apis?: ( + originalImplementation: JWTAPIInterface, + builder?: OverrideableBuilder + ) => JWTAPIInterface + } + } + } +} + +export interface SessionRequest extends BaseRequest { + session?: SessionContainerInterface +} + +export interface ErrorHandlerMiddleware { + (message: string, request: BaseRequest, response: BaseResponse): Promise +} + +export interface TokenTheftErrorHandlerMiddleware { + (sessionHandle: string, userId: string, request: BaseRequest, response: BaseResponse): Promise +} + +export interface InvalidClaimErrorHandlerMiddleware { + (validatorErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse): Promise +} + +export interface NormalisedErrorHandlers { + onUnauthorised: ErrorHandlerMiddleware + onTryRefreshToken: ErrorHandlerMiddleware + onTokenTheftDetected: TokenTheftErrorHandlerMiddleware + onInvalidClaim: InvalidClaimErrorHandlerMiddleware +} + +export interface VerifySessionOptions { + antiCsrfCheck?: boolean + sessionRequired?: boolean + overrideGlobalClaimValidators?: ( + globalClaimValidators: SessionClaimValidator[], + session: SessionContainerInterface, + userContext: any + ) => Promise | SessionClaimValidator[] +} + +export interface RecipeInterface { + createNewSession(input: { + req: BaseRequest + res: BaseResponse + userId: string + accessTokenPayload?: any + sessionData?: any + userContext: any + }): Promise + + getGlobalClaimValidators(input: { + userId: string + claimValidatorsAddedByOtherRecipes: SessionClaimValidator[] + userContext: any + }): Promise | SessionClaimValidator[] + + getSession(input: { + req: BaseRequest + res: BaseResponse + options?: VerifySessionOptions + userContext: any + }): Promise + + refreshSession(input: { + req: BaseRequest + res: BaseResponse + userContext: any + }): Promise + /** + * Used to retrieve all session information for a given session handle. Can be used in place of: + * - getSessionData + * - getAccessTokenPayload + * + * Returns undefined if the sessionHandle does not exist + */ + getSessionInformation(input: { sessionHandle: string; userContext: any }): Promise + + revokeAllSessionsForUser(input: { userId: string; userContext: any }): Promise + + getAllSessionHandlesForUser(input: { userId: string; userContext: any }): Promise + + revokeSession(input: { sessionHandle: string; userContext: any }): Promise + + revokeMultipleSessions(input: { sessionHandles: string[]; userContext: any }): Promise + + // Returns false if the sessionHandle does not exist + updateSessionData(input: { sessionHandle: string; newSessionData: any; userContext: any }): Promise + + /** + * @deprecated Use mergeIntoAccessTokenPayload instead + * @returns {Promise} Returns false if the sessionHandle does not exist + */ + updateAccessTokenPayload(input: { + sessionHandle: string + newAccessTokenPayload: any + userContext: any + }): Promise + + mergeIntoAccessTokenPayload(input: { + sessionHandle: string + accessTokenPayloadUpdate: JSONObject + userContext: any + }): Promise + + /** + * @returns {Promise} Returns false if the sessionHandle does not exist + */ + regenerateAccessToken(input: { + accessToken: string + newAccessTokenPayload?: any + userContext: any + }): Promise< + | { + status: 'OK' + session: { + handle: string + userId: string + userDataInJWT: any + } + accessToken?: { + token: string + expiry: number + createdTime: number + } + } + | undefined + > + + getAccessTokenLifeTimeMS(input: { userContext: any }): Promise + + getRefreshTokenLifeTimeMS(input: { userContext: any }): Promise + + validateClaims(input: { + userId: string + accessTokenPayload: any + claimValidators: SessionClaimValidator[] + userContext: any + }): Promise<{ + invalidClaims: ClaimValidationError[] + accessTokenPayloadUpdate?: any + }> + + validateClaimsInJWTPayload(input: { + userId: string + jwtPayload: JSONObject + claimValidators: SessionClaimValidator[] + userContext: any + }): Promise<{ + status: 'OK' + invalidClaims: ClaimValidationError[] + }> + + fetchAndSetClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise + setClaimValue(input: { + sessionHandle: string + claim: SessionClaim + value: T + userContext: any + }): Promise + + getClaimValue(input: { + sessionHandle: string + claim: SessionClaim + userContext: any + }): Promise< + | { + status: 'SESSION_DOES_NOT_EXIST_ERROR' + } + | { + status: 'OK' + value: T | undefined + } + > + + removeClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise +} + +export interface SessionContainerInterface { + revokeSession(userContext?: any): Promise + + getSessionData(userContext?: any): Promise + + updateSessionData(newSessionData: any, userContext?: any): Promise + + getUserId(userContext?: any): string + + getAccessTokenPayload(userContext?: any): any + + getHandle(userContext?: any): string + + getAccessToken(userContext?: any): string + + /** + * @deprecated Use mergeIntoAccessTokenPayload instead + */ + updateAccessTokenPayload(newAccessTokenPayload: any, userContext?: any): Promise + mergeIntoAccessTokenPayload(accessTokenPayloadUpdate?: JSONObject, userContext?: any): Promise + + getTimeCreated(userContext?: any): Promise + + getExpiry(userContext?: any): Promise + + assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise + fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise + setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise + getClaimValue(claim: SessionClaim, userContext?: any): Promise + removeClaim(claim: SessionClaim, userContext?: any): Promise +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + config: TypeNormalisedInput + recipeId: string + isInServerlessEnv: boolean + req: BaseRequest + res: BaseResponse +} + +export interface APIInterface { + /** + * We do not add a GeneralErrorResponse response to this API + * since it's not something that is directly called by the user on the + * frontend anyway + */ + refreshPOST: undefined | ((input: { options: APIOptions; userContext: any }) => Promise) + + signOutPOST: + | undefined + | ((input: { + options: APIOptions + // the reason we make this optional is cause it allows users to do something in + // case a session does not exist and the sign out button is pressed. It is + // rare that something needs to be done in this case, but making it like this + // has little disadvantages. + session: SessionContainerInterface | undefined + userContext: any + }) => Promise< + | { + status: 'OK' + } + | GeneralErrorResponse + >) + + verifySession(input: { + verifySessionOptions: VerifySessionOptions | undefined + options: APIOptions + userContext: any + }): Promise +} + +export interface SessionInformation { + sessionHandle: string + userId: string + sessionData: any + expiry: number + accessTokenPayload: any + timeCreated: number +} + +export type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: JSONValue } +export interface ClaimValidationError { + id: string + reason?: JSONValue +} + +export type SessionClaimValidator = ( + | // We split the type like this to express that either both claim and shouldRefetch is defined or neither. + { + claim: SessionClaim + /** + * Decides if we need to refetch the claim value before checking the payload with `isValid`. + * E.g.: if the information in the payload is expired, or is not sufficient for this check. + */ + shouldRefetch: (payload: any, userContext: any) => Promise | boolean + } + | {} +) & { + id: string + /** + * Decides if the claim is valid based on the payload (and not checking DB or anything else) + */ + validate: (payload: any, userContext: any) => Promise +} + +export abstract class SessionClaim { + constructor(public readonly key: string) { } + + /** + * This methods fetches the current value of this claim for the user. + * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database + * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. + */ + abstract fetchValue(userId: string, userContext: any): Promise | T | undefined + + /** + * Saves the provided value into the payload, by cloning and updating the entire object. + * + * @returns The modified payload object + */ + abstract addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject + + /** + * Removes the claim from the payload by setting it to null, so mergeIntoAccessTokenPayload clears it + * + * @returns The modified payload object + */ + abstract removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject + + /** + * Removes the claim from the payload, by cloning and updating the entire object. + * + * @returns The modified payload object + */ + abstract removeFromPayload(payload: JSONObject, userContext?: any): JSONObject + + /** + * Gets the value of the claim stored in the payload + * + * @returns Claim value + */ + abstract getValueFromPayload(payload: JSONObject, userContext: any): T | undefined + + async build(userId: string, userContext?: any): Promise { + const value = await this.fetchValue(userId, userContext) + + if (value === undefined) + return {} + + return this.addToPayload_internal({}, value, userContext) + } +} diff --git a/src/recipe/session/utils.ts b/src/recipe/session/utils.ts new file mode 100644 index 000000000..28d02fd66 --- /dev/null +++ b/src/recipe/session/utils.ts @@ -0,0 +1,342 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { URL } from 'url' +import NormalisedURLPath from '../../normalisedURLPath' +import { NormalisedAppinfo } from '../../types' +import { isAnIpAddress, sendNon200Response, sendNon200ResponseWithMessage } from '../../utils' +import { BaseRequest, BaseResponse } from '../../framework' +import { logDebugMessage } from '../../logger' +import { + APIInterface, ClaimValidationError, + CreateOrRefreshAPIResponse, + NormalisedErrorHandlers, + RecipeInterface, + SessionClaimValidator, + SessionContainerInterface, + TokenTransferMethod, + TypeInput, + TypeNormalisedInput, + VerifySessionOptions, +} from './types' +import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY, JWT_RESERVED_KEY_USE_ERROR_MESSAGE } from './with-jwt/constants' +import { REFRESH_API_PATH } from './constants' +import SessionRecipe from './recipe' +import { getAuthModeFromHeader, setAntiCsrfTokenInHeaders, setFrontTokenInHeaders, setToken } from './cookieAndHeaders' + +export async function sendTryRefreshTokenResponse( + recipeInstance: SessionRecipe, + _: string, + __: BaseRequest, + response: BaseResponse, +) { + sendNon200ResponseWithMessage(response, 'try refresh token', recipeInstance.config.sessionExpiredStatusCode) +} + +export async function sendUnauthorisedResponse( + recipeInstance: SessionRecipe, + _: string, + __: BaseRequest, + response: BaseResponse, +) { + sendNon200ResponseWithMessage(response, 'unauthorised', recipeInstance.config.sessionExpiredStatusCode) +} + +export async function sendInvalidClaimResponse( + recipeInstance: SessionRecipe, + claimValidationErrors: ClaimValidationError[], + __: BaseRequest, + response: BaseResponse, +) { + sendNon200Response(response, recipeInstance.config.invalidClaimStatusCode, { + message: 'invalid claim', + claimValidationErrors, + }) +} + +export async function sendTokenTheftDetectedResponse( + recipeInstance: SessionRecipe, + sessionHandle: string, + _: string, + __: BaseRequest, + response: BaseResponse, +) { + await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }) + sendNon200ResponseWithMessage(response, 'token theft detected', recipeInstance.config.sessionExpiredStatusCode) +} + +export function normaliseSessionScopeOrThrowError(sessionScope: string): string { + function helper(sessionScope: string): string { + sessionScope = sessionScope.trim().toLowerCase() + + // first we convert it to a URL so that we can use the URL class + if (sessionScope.startsWith('.')) + sessionScope = sessionScope.substr(1) + + if (!sessionScope.startsWith('http://') && !sessionScope.startsWith('https://')) + sessionScope = `http://${sessionScope}` + + try { + const urlObj = new URL(sessionScope) + sessionScope = urlObj.hostname + + // remove leading dot + if (sessionScope.startsWith('.')) + sessionScope = sessionScope.substr(1) + + return sessionScope + } + catch (err) { + throw new Error('Please provide a valid sessionScope') + } + } + + const noDotNormalised = helper(sessionScope) + + if (noDotNormalised === 'localhost' || isAnIpAddress(noDotNormalised)) + return noDotNormalised + + if (sessionScope.startsWith('.')) + return `.${noDotNormalised}` + + return noDotNormalised +} + +export function getURLProtocol(url: string): string { + const urlObj = new URL(url) + return urlObj.protocol +} + +export function validateAndNormaliseUserInput( + recipeInstance: SessionRecipe, + appInfo: NormalisedAppinfo, + config?: TypeInput, +): TypeNormalisedInput { + const cookieDomain + = (config === undefined || config.cookieDomain === undefined) + ? undefined + : normaliseSessionScopeOrThrowError(config.cookieDomain) + const accessTokenPath + = (config === undefined || config.accessTokenPath === undefined) + ? new NormalisedURLPath('/') + : new NormalisedURLPath(config.accessTokenPath) + const protocolOfAPIDomain = getURLProtocol(appInfo.apiDomain.getAsStringDangerous()) + const protocolOfWebsiteDomain = getURLProtocol(appInfo.websiteDomain.getAsStringDangerous()) + + let cookieSameSite: 'strict' | 'lax' | 'none' + = (appInfo.topLevelAPIDomain !== appInfo.topLevelWebsiteDomain || protocolOfAPIDomain !== protocolOfWebsiteDomain) + ? 'none' + : 'lax' + cookieSameSite + = (config === undefined || config.cookieSameSite === undefined) + ? cookieSameSite + : normaliseSameSiteOrThrowError(config.cookieSameSite) + + const cookieSecure + = (config === undefined || config.cookieSecure === undefined) + ? appInfo.apiDomain.getAsStringDangerous().startsWith('https') + : config.cookieSecure + + const sessionExpiredStatusCode + = (config === undefined || config.sessionExpiredStatusCode === undefined) ? 401 : config.sessionExpiredStatusCode + const invalidClaimStatusCode = config?.invalidClaimStatusCode ?? 403 + + if (sessionExpiredStatusCode === invalidClaimStatusCode) + throw new Error('sessionExpiredStatusCode and sessionExpiredStatusCode must be different') + + if (config !== undefined && config.antiCsrf !== undefined) { + if (config.antiCsrf !== 'NONE' && config.antiCsrf !== 'VIA_CUSTOM_HEADER' && config.antiCsrf !== 'VIA_TOKEN') + throw new Error('antiCsrf config must be one of \'NONE\' or \'VIA_CUSTOM_HEADER\' or \'VIA_TOKEN\'') + } + + const antiCsrf: 'VIA_TOKEN' | 'VIA_CUSTOM_HEADER' | 'NONE' + = (config === undefined || config.antiCsrf === undefined) + ? cookieSameSite === 'none' + ? 'VIA_CUSTOM_HEADER' + : 'NONE' + : config.antiCsrf + + const errorHandlers: NormalisedErrorHandlers = { + onTokenTheftDetected: async ( + sessionHandle: string, + userId: string, + request: BaseRequest, + response: BaseResponse, + ) => { + return await sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, userId, request, response) + }, + onTryRefreshToken: async (message: string, request: BaseRequest, response: BaseResponse) => { + return await sendTryRefreshTokenResponse(recipeInstance, message, request, response) + }, + onUnauthorised: async (message: string, request: BaseRequest, response: BaseResponse) => { + return await sendUnauthorisedResponse(recipeInstance, message, request, response) + }, + onInvalidClaim: (validationErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse) => { + return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response) + }, + } + if (config !== undefined && config.errorHandlers !== undefined) { + if (config.errorHandlers.onTokenTheftDetected !== undefined) + errorHandlers.onTokenTheftDetected = config.errorHandlers.onTokenTheftDetected + + if (config.errorHandlers.onUnauthorised !== undefined) + errorHandlers.onUnauthorised = config.errorHandlers.onUnauthorised + + if (config.errorHandlers.onInvalidClaim !== undefined) + errorHandlers.onInvalidClaim = config.errorHandlers.onInvalidClaim + } + + let enableJWT = false + let accessTokenPayloadJWTPropertyName = 'jwt' + let issuer: string | undefined + + if (config !== undefined && config.jwt !== undefined && config.jwt.enable === true) { + enableJWT = true + const jwtPropertyName = config.jwt.propertyNameInAccessTokenPayload + issuer = config.jwt.issuer + + if (jwtPropertyName !== undefined) { + if (jwtPropertyName === ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY) + throw new Error(JWT_RESERVED_KEY_USE_ERROR_MESSAGE) + + accessTokenPayloadJWTPropertyName = jwtPropertyName + } + } + + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + return { + refreshTokenPath: appInfo.apiBasePath.appendPath(new NormalisedURLPath(REFRESH_API_PATH)), + accessTokenPath, + getTokenTransferMethod: + config?.getTokenTransferMethod === undefined + ? defaultGetTokenTransferMethod + : config.getTokenTransferMethod, + cookieDomain, + cookieSameSite, + cookieSecure, + sessionExpiredStatusCode, + errorHandlers, + antiCsrf, + override, + invalidClaimStatusCode, + jwt: { + enable: enableJWT, + propertyNameInAccessTokenPayload: accessTokenPayloadJWTPropertyName, + issuer, + }, + } +} + +export function normaliseSameSiteOrThrowError(sameSite: string): 'strict' | 'lax' | 'none' { + sameSite = sameSite.trim() + sameSite = sameSite.toLocaleLowerCase() + if (sameSite !== 'strict' && sameSite !== 'lax' && sameSite !== 'none') + throw new Error('cookie same site must be one of "strict", "lax", or "none"') + + return sameSite +} + +export function attachTokensToResponse( + config: TypeNormalisedInput, + res: BaseResponse, + response: CreateOrRefreshAPIResponse, + transferMethod: TokenTransferMethod, +) { + const accessToken = response.accessToken + const refreshToken = response.refreshToken + setFrontTokenInHeaders(res, response.session.userId, response.accessToken.expiry, response.session.userDataInJWT) + setToken( + config, + res, + 'access', + accessToken.token, + // We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + // This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway. + // Even if the token is expired the presence of the token indicates that the user could have a valid refresh + // Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + Date.now() + 3153600000000, + transferMethod, + ) + setToken(config, res, 'refresh', refreshToken.token, refreshToken.expiry, transferMethod) + if (response.antiCsrfToken !== undefined) + setAntiCsrfTokenInHeaders(res, response.antiCsrfToken) +} + +export async function getRequiredClaimValidators( + session: SessionContainerInterface, + overrideGlobalClaimValidators: VerifySessionOptions['overrideGlobalClaimValidators'], + userContext: any, +) { + const claimValidatorsAddedByOtherRecipes = SessionRecipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes() + const globalClaimValidators: SessionClaimValidator[] = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getGlobalClaimValidators( + { + userId: session.getUserId(), + claimValidatorsAddedByOtherRecipes, + userContext, + }, + ) + + return overrideGlobalClaimValidators !== undefined + ? await overrideGlobalClaimValidators(globalClaimValidators, session, userContext) + : globalClaimValidators +} + +export async function validateClaimsInPayload( + claimValidators: SessionClaimValidator[], + newAccessTokenPayload: any, + userContext: any, +) { + const validationErrors = [] + for (const validator of claimValidators) { + const claimValidationResult = await validator.validate(newAccessTokenPayload, userContext) + logDebugMessage( + `validateClaimsInPayload ${validator.id} validation res ${JSON.stringify(claimValidationResult)}`, + ) + if (!claimValidationResult.isValid) { + validationErrors.push({ + id: validator.id, + reason: claimValidationResult.reason, + }) + } + } + return validationErrors +} + +function defaultGetTokenTransferMethod({ + req, + forCreateNewSession, +}: { + req: BaseRequest + forCreateNewSession: boolean +}): TokenTransferMethod | 'any' { + // We allow fallback (checking headers then cookies) by default when validating + if (!forCreateNewSession) + return 'any' + + // In create new session we respect the frontend preference by default + switch (getAuthModeFromHeader(req)) { + case 'header': + return 'header' + case 'cookie': + return 'cookie' + default: + return 'any' + } +} diff --git a/src/recipe/session/with-jwt/constants.ts b/src/recipe/session/with-jwt/constants.ts new file mode 100644 index 000000000..56fdb55ae --- /dev/null +++ b/src/recipe/session/with-jwt/constants.ts @@ -0,0 +1,38 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +/* + This key is used to determine the property name used when adding the jwt to the access token payload + For example if the Session recipe is initialised with config + { + ... + jwt: { + enable: true, + propertyNameInAccessTokenPayload: "jwtKey", + }, + ... + } + + The access token payload after creating a session would look like + { + ... + jwtKey: "JWT_STRING", + _jwtPName: "jwtKey", + } + + When trying to refresh the session or updating the access token payload, this key is used to determine and retrieve + the exsiting JWT from the access token payload. +*/ +export const ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY = '_jwtPName' +export const JWT_RESERVED_KEY_USE_ERROR_MESSAGE = `${ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY} is a reserved property name, please use a different key name for the jwt` diff --git a/src/recipe/session/with-jwt/index.ts b/src/recipe/session/with-jwt/index.ts new file mode 100644 index 000000000..2f98f574d --- /dev/null +++ b/src/recipe/session/with-jwt/index.ts @@ -0,0 +1,17 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OriginalImplementation from './recipeImplementation' + +export default OriginalImplementation diff --git a/src/recipe/session/with-jwt/recipeImplementation.ts b/src/recipe/session/with-jwt/recipeImplementation.ts new file mode 100644 index 000000000..e7d3345b5 --- /dev/null +++ b/src/recipe/session/with-jwt/recipeImplementation.ts @@ -0,0 +1,251 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import * as assert from 'assert' +import { decode } from 'jsonwebtoken' + +import { RecipeInterface } from '../' +import { RecipeInterface as OpenIdRecipeInterface } from '../../openid/types' +import { SessionContainerInterface, SessionInformation, TypeNormalisedInput, VerifySessionOptions } from '../types' +import { JSONObject } from '../../../types' +import { BaseResponse } from '../../../framework/response' +import { BaseRequest } from '../../../framework/request' +import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY } from './constants' +import SessionClassWithJWT from './sessionClass' +import { addJWTToAccessTokenPayload } from './utils' + +// Time difference between JWT expiry and access token expiry (JWT expiry = access token expiry + EXPIRY_OFFSET_SECONDS) +let EXPIRY_OFFSET_SECONDS = 30 + +// This function should only be used during testing +export function setJWTExpiryOffsetSecondsForTesting(offset: number) { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + EXPIRY_OFFSET_SECONDS = offset +} + +export default function ( + originalImplementation: RecipeInterface, + openIdRecipeImplementation: OpenIdRecipeInterface, + config: TypeNormalisedInput, +): RecipeInterface { + function getJWTExpiry(accessTokenExpiry: number): number { + return accessTokenExpiry + EXPIRY_OFFSET_SECONDS + } + + async function jwtAwareUpdateAccessTokenPayload( + sessionInformation: SessionInformation, + newAccessTokenPayload: any, + userContext: any, + ) { + const accessTokenPayload = sessionInformation.accessTokenPayload + + const existingJwtPropertyName = accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY] + + if (existingJwtPropertyName === undefined) { + return await originalImplementation.updateAccessTokenPayload({ + sessionHandle: sessionInformation.sessionHandle, + newAccessTokenPayload, + userContext, + }) + } + + const existingJwt = accessTokenPayload[existingJwtPropertyName] + assert.notStrictEqual(existingJwt, undefined) + + const currentTimeInSeconds = Date.now() / 1000 + const decodedPayload = decode(existingJwt, { json: true }) + + // decode possibly returns null + if (decodedPayload === null || decodedPayload.exp === undefined) + throw new Error('Error reading JWT from session') + + let jwtExpiry = decodedPayload.exp - currentTimeInSeconds + + if (jwtExpiry <= 0) { + // it can come here if someone calls this function well after + // the access token and the jwt payload have expired. In this case, + // we still want the jwt payload to update, but the resulting JWT should + // not be alive for too long (since it's expired already). So we set it to + // 1 second lifetime. + jwtExpiry = 1 + } + + newAccessTokenPayload = await addJWTToAccessTokenPayload({ + accessTokenPayload: newAccessTokenPayload, + jwtExpiry, + userId: sessionInformation.userId, + jwtPropertyName: existingJwtPropertyName, + openIdRecipeImplementation, + userContext, + }) + + return await originalImplementation.updateAccessTokenPayload({ + sessionHandle: sessionInformation.sessionHandle, + newAccessTokenPayload, + userContext, + }) + } + + return { + ...originalImplementation, + async createNewSession( + this: RecipeInterface, + { + req, + res, + userId, + accessTokenPayload, + sessionData, + userContext, + }: { + req: BaseRequest + res: BaseResponse + userId: string + accessTokenPayload?: any + sessionData?: any + userContext: any + }, + ): Promise { + accessTokenPayload + = (accessTokenPayload === null || accessTokenPayload === undefined) ? {} : accessTokenPayload + const accessTokenValidityInSeconds = Math.ceil((await this.getAccessTokenLifeTimeMS({ userContext })) / 1000) + + accessTokenPayload = await addJWTToAccessTokenPayload({ + accessTokenPayload, + jwtExpiry: getJWTExpiry(accessTokenValidityInSeconds), + userId, + jwtPropertyName: config.jwt.propertyNameInAccessTokenPayload, + openIdRecipeImplementation, + userContext, + }) + + const sessionContainer = await originalImplementation.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + userContext, + }) + + return new SessionClassWithJWT(sessionContainer, openIdRecipeImplementation) + }, + async getSession( + this: RecipeInterface, + { + req, + res, + options, + userContext, + }: { + req: BaseRequest + res: BaseResponse + options?: VerifySessionOptions + userContext: any + }, + ): Promise { + const sessionContainer = await originalImplementation.getSession({ req, res, options, userContext }) + + if (sessionContainer === undefined) + return undefined + + return new SessionClassWithJWT(sessionContainer, openIdRecipeImplementation) + }, + async refreshSession( + this: RecipeInterface, + { + req, + res, + userContext, + }: { + req: BaseRequest + res: BaseResponse + userContext: any + }, + ): Promise { + const accessTokenValidityInSeconds = Math.ceil((await this.getAccessTokenLifeTimeMS({ userContext })) / 1000) + + // Refresh session first because this will create a new access token + const newSession = await originalImplementation.refreshSession({ req, res, userContext }) + let accessTokenPayload = newSession.getAccessTokenPayload() + + accessTokenPayload = await addJWTToAccessTokenPayload({ + accessTokenPayload, + jwtExpiry: getJWTExpiry(accessTokenValidityInSeconds), + userId: newSession.getUserId(), + jwtPropertyName: config.jwt.propertyNameInAccessTokenPayload, + openIdRecipeImplementation, + userContext, + }) + + await newSession.updateAccessTokenPayload(accessTokenPayload) + + return new SessionClassWithJWT(newSession, openIdRecipeImplementation) + }, + + async mergeIntoAccessTokenPayload( + this: RecipeInterface, + { + sessionHandle, + accessTokenPayloadUpdate, + userContext, + }: { + sessionHandle: string + accessTokenPayloadUpdate: JSONObject + userContext: any + }, + ): Promise { + const sessionInformation = await this.getSessionInformation({ sessionHandle, userContext }) + + if (!sessionInformation) + return false + + const newAccessTokenPayload = { ...sessionInformation.accessTokenPayload, ...accessTokenPayloadUpdate } + for (const key of Object.keys(accessTokenPayloadUpdate)) { + if (accessTokenPayloadUpdate[key] === null) + delete newAccessTokenPayload[key] + } + + return jwtAwareUpdateAccessTokenPayload(sessionInformation, newAccessTokenPayload, userContext) + }, + + /** + * @deprecated use mergeIntoAccessTokenPayload instead + */ + async updateAccessTokenPayload( + this: RecipeInterface, + { + sessionHandle, + newAccessTokenPayload, + userContext, + }: { + sessionHandle: string + newAccessTokenPayload: any + userContext: any + }, + ): Promise { + newAccessTokenPayload + = (newAccessTokenPayload === null || newAccessTokenPayload === undefined) ? {} : newAccessTokenPayload + + const sessionInformation = await this.getSessionInformation({ sessionHandle, userContext }) + + if (!sessionInformation) + return false + + return jwtAwareUpdateAccessTokenPayload(sessionInformation, newAccessTokenPayload, userContext) + }, + } +} diff --git a/src/recipe/session/with-jwt/sessionClass.ts b/src/recipe/session/with-jwt/sessionClass.ts new file mode 100644 index 000000000..815e25a87 --- /dev/null +++ b/src/recipe/session/with-jwt/sessionClass.ts @@ -0,0 +1,174 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import * as assert from 'assert' +import { decode } from 'jsonwebtoken' + +import { RecipeInterface as OpenIdRecipeInterface } from '../../openid/types' +import { SessionClaim, SessionClaimValidator, SessionContainerInterface } from '../types' +import STError from '../error' +import SessionRecipe from '../recipe' +import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY } from './constants' +import { addJWTToAccessTokenPayload } from './utils' + +export default class SessionClassWithJWT implements SessionContainerInterface { + constructor( + private readonly originalSessionClass: SessionContainerInterface, + private readonly openIdRecipeImplementation: OpenIdRecipeInterface, + ) {} + + revokeSession(userContext?: any): Promise { + return this.originalSessionClass.revokeSession(userContext) + } + + getSessionData(userContext?: any): Promise { + return this.originalSessionClass.getSessionData(userContext) + } + + updateSessionData(newSessionData: any, userContext?: any): Promise { + return this.originalSessionClass.updateSessionData(newSessionData, userContext) + } + + getUserId(userContext?: any): string { + return this.originalSessionClass.getUserId(userContext) + } + + getAccessTokenPayload(userContext?: any) { + return this.originalSessionClass.getAccessTokenPayload(userContext) + } + + getHandle(userContext?: any): string { + return this.originalSessionClass.getHandle(userContext) + } + + getAccessToken(userContext?: any): string { + return this.originalSessionClass.getAccessToken(userContext) + } + + getTimeCreated(userContext?: any): Promise { + return this.originalSessionClass.getTimeCreated(userContext) + } + + getExpiry(userContext?: any): Promise { + return this.originalSessionClass.getExpiry(userContext) + } + + // We copy the implementation here, since we want to override updateAccessTokenPayload + async assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise { + const validateClaimResponse = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.validateClaims({ + accessTokenPayload: this.getAccessTokenPayload(userContext), + userId: this.getUserId(userContext), + claimValidators, + userContext, + }) + + if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) + await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext) + + if (validateClaimResponse.invalidClaims.length !== 0) { + throw new STError({ + type: 'INVALID_CLAIMS', + message: 'INVALID_CLAIMS', + payload: validateClaimResponse.invalidClaims, + }) + } + } + + // We copy the implementation here, since we want to override updateAccessTokenPayload + async fetchAndSetClaim(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any) { + const update = await claim.build(this.getUserId(userContext), userContext) + return this.mergeIntoAccessTokenPayload(update, userContext) + } + + // We copy the implementation here, since we want to override updateAccessTokenPayload + setClaimValue(this: SessionClassWithJWT, claim: SessionClaim, value: T, userContext?: any) { + const update = claim.addToPayload_internal({}, value, userContext) + return this.mergeIntoAccessTokenPayload(update, userContext) + } + + // We copy the implementation here, since we want to override updateAccessTokenPayload + async getClaimValue(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any) { + return claim.getValueFromPayload(await this.getAccessTokenPayload(userContext), userContext) + } + + // We copy the implementation here, since we want to override updateAccessTokenPayload + removeClaim(this: SessionClassWithJWT, claim: SessionClaim, userContext?: any) { + const update = claim.removeFromPayloadByMerge_internal({}, userContext) + return this.mergeIntoAccessTokenPayload(update, userContext) + } + + // We copy the implementation here, since we want to override updateAccessTokenPayload + async mergeIntoAccessTokenPayload( + this: SessionClassWithJWT, + accessTokenPayloadUpdate: any, + userContext?: any, + ): Promise { + const updatedPayload = { ...this.getAccessTokenPayload(userContext), ...accessTokenPayloadUpdate } + for (const key of Object.keys(accessTokenPayloadUpdate)) { + if (accessTokenPayloadUpdate[key] === null) + delete updatedPayload[key] + } + + await this.updateAccessTokenPayload(updatedPayload, userContext) + } + + // TODO: figure out a proper way to override just this function + /** + * @deprecated use mergeIntoAccessTokenPayload instead + */ + async updateAccessTokenPayload( + this: SessionClassWithJWT, + newAccessTokenPayload: any | undefined, + userContext?: any, + ): Promise { + newAccessTokenPayload + = (newAccessTokenPayload === null || newAccessTokenPayload === undefined) ? {} : newAccessTokenPayload + const accessTokenPayload = this.getAccessTokenPayload(userContext) + const jwtPropertyName = accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY] + + if (jwtPropertyName === undefined) + return this.originalSessionClass.updateAccessTokenPayload(newAccessTokenPayload, userContext) + + const existingJWT = accessTokenPayload[jwtPropertyName] + assert.notStrictEqual(existingJWT, undefined) + + const currentTimeInSeconds = Date.now() / 1000 + const decodedPayload = decode(existingJWT, { json: true }) + + // JsonWebToken.decode possibly returns null + if (decodedPayload === null || decodedPayload.exp === undefined) + throw new Error('Error reading JWT from session') + + let jwtExpiry = decodedPayload.exp - currentTimeInSeconds + + if (jwtExpiry <= 0) { + // it can come here if someone calls this function well after + // the access token and the jwt payload have expired (which can happen if an API takes a VERY long time). In this case, we still want the jwt payload to update, but the resulting JWT should + // not be alive for too long (since it's expired already). So we set it to + // 1 second lifetime. + jwtExpiry = 1 + } + + newAccessTokenPayload = await addJWTToAccessTokenPayload({ + accessTokenPayload: newAccessTokenPayload, + jwtExpiry, + userId: this.getUserId(), + jwtPropertyName, + openIdRecipeImplementation: this.openIdRecipeImplementation, + userContext, + }) + + await this.originalSessionClass.updateAccessTokenPayload(newAccessTokenPayload, userContext) + } +} diff --git a/src/recipe/session/with-jwt/utils.ts b/src/recipe/session/with-jwt/utils.ts new file mode 100644 index 000000000..69f9f3f2d --- /dev/null +++ b/src/recipe/session/with-jwt/utils.ts @@ -0,0 +1,85 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { RecipeInterface as OpenIdRecipeInterface } from '../../openid/types' +import { ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY } from './constants' + +export async function addJWTToAccessTokenPayload({ + accessTokenPayload, + jwtExpiry, + userId, + jwtPropertyName, + openIdRecipeImplementation, + userContext, +}: { + accessTokenPayload: any + jwtExpiry: number + userId: string + jwtPropertyName: string + openIdRecipeImplementation: OpenIdRecipeInterface + userContext: any +}): Promise { + // If jwtPropertyName is not undefined it means that the JWT was added to the access token payload already + const existingJwtPropertyName = accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY] + + if (existingJwtPropertyName !== undefined) { + // Delete the old JWT and the old property name + delete accessTokenPayload[existingJwtPropertyName] + delete accessTokenPayload[ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY] + } + + // Create the JWT + const jwtResponse = await openIdRecipeImplementation.createJWT({ + payload: { + /* + We add our claims before the user provided ones so that if they use the same claims + then the final payload will use the values they provide + */ + sub: userId, + ...accessTokenPayload, + }, + validitySeconds: jwtExpiry, + userContext, + }) + + if (jwtResponse.status === 'UNSUPPORTED_ALGORITHM_ERROR') { + // Should never come here + throw new Error('JWT Signing algorithm not supported') + } + + // Add the jwt and the property name to the access token payload + accessTokenPayload = { + ...accessTokenPayload, + /* + We add the JWT after the user defined keys because we want to make sure that it never + gets overwritten by a user defined key. Using the same key as the one configured (or defaulting) + for the JWT should be considered a dev error + + ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY indicates a reserved key used to determine the property name + with which the JWT is set, used to retrieve the JWT from the access token payload during refresg and + updateAccessTokenPayload + + Note: If the user has multiple overrides each with a unique propertyNameInAccessTokenPayload, the logic + for checking the existing JWT when refreshing the session or updating the access token payload will not work. + This is because even though the jwt itself would be created with unique property names, the _jwtPName value would + always be overwritten by the override that runs last and when retrieving the jwt using that key name it cannot be + guaranteed that the right JWT is returned. This case is considered to be a rare requirement and we assume + that users will not need multiple JWT representations of their access token payload. + */ + [jwtPropertyName]: jwtResponse.jwt, + [ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY]: jwtPropertyName, + } + + return accessTokenPayload +} diff --git a/src/recipe/thirdparty/api/appleRedirect.ts b/src/recipe/thirdparty/api/appleRedirect.ts new file mode 100644 index 000000000..f1f30ec26 --- /dev/null +++ b/src/recipe/thirdparty/api/appleRedirect.ts @@ -0,0 +1,40 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { APIInterface, APIOptions } from '../' +import { makeDefaultUserContextFromAPI } from '../../../utils' + +export default async function appleRedirectHandler( + apiImplementation: APIInterface, + options: APIOptions, +): Promise { + if (apiImplementation.appleRedirectHandlerPOST === undefined) + return false + + const body = await options.req.getFormData() + + const state = body.state + const code = body.code + + // this will redirect the user... + await apiImplementation.appleRedirectHandlerPOST({ + code, + state, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + return true +} diff --git a/src/recipe/thirdparty/api/authorisationUrl.ts b/src/recipe/thirdparty/api/authorisationUrl.ts new file mode 100644 index 000000000..b09812c84 --- /dev/null +++ b/src/recipe/thirdparty/api/authorisationUrl.ts @@ -0,0 +1,53 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import STError from '../error' +import { APIInterface, APIOptions } from '../' +import { findRightProvider } from '../utils' + +export default async function authorisationUrlAPI( + apiImplementation: APIInterface, + options: APIOptions, +): Promise { + if (apiImplementation.authorisationUrlGET === undefined) + return false + + const thirdPartyId = options.req.getKeyValueFromQuery('thirdPartyId') + + if (thirdPartyId === undefined || typeof thirdPartyId !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the thirdPartyId as a GET param', + }) + } + + const provider = findRightProvider(options.providers, thirdPartyId, undefined) + if (provider === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: `The third party provider ${thirdPartyId} seems to be missing from the backend configs.`, + }) + } + + const result = await apiImplementation.authorisationUrlGET({ + provider, + options, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + send200Response(options.res, result) + return true +} diff --git a/src/recipe/thirdparty/api/implementation.ts b/src/recipe/thirdparty/api/implementation.ts new file mode 100644 index 000000000..28a9ed038 --- /dev/null +++ b/src/recipe/thirdparty/api/implementation.ts @@ -0,0 +1,229 @@ +import { URLSearchParams } from 'url' +import * as qs from 'querystring' +import * as axios from 'axios' +import { APIInterface, APIOptions, TypeProvider, User } from '../' +import Session from '../../session' +import { SessionContainerInterface } from '../../session/types' +import { GeneralErrorResponse } from '../../../types' +import EmailVerification from '../../emailverification/recipe' + +const DEV_OAUTH_AUTHORIZATION_URL = 'https://supertokens.io/dev/oauth/redirect-to-provider' +const DEV_OAUTH_REDIRECT_URL = 'https://supertokens.io/dev/oauth/redirect-to-app' + +// If Third Party login is used with one of the following development keys, then the dev authorization url and the redirect url will be used. +const DEV_OAUTH_CLIENT_IDS = [ + '1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com', // google + '467101b197249757c71f', // github +] +const DEV_KEY_IDENTIFIER = '4398792-' + +export default function getAPIInterface(): APIInterface { + return { + async authorisationUrlGET({ + provider, + options, + userContext, + }: { + provider: TypeProvider + options: APIOptions + userContext: any + }): Promise< + | { + status: 'OK' + url: string + } + | GeneralErrorResponse + > { + const providerInfo = provider.get(undefined, undefined, userContext) + + const params: { [key: string]: string } = {} + const keys = Object.keys(providerInfo.authorisationRedirect.params) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const value = providerInfo.authorisationRedirect.params[key] + params[key] = typeof value === 'function' ? await value(options.req.original) : value + } + if ( + providerInfo.getRedirectURI !== undefined + && !isUsingDevelopmentClientId(providerInfo.getClientId(userContext)) + ) { + // the backend wants to set the redirectURI - so we set that here. + + // we add the not development keys because the oauth provider will + // redirect to supertokens.io's URL which will redirect the app + // to the the user's website, which will handle the callback as usual. + // If we add this, then instead, the supertokens' site will redirect + // the user to this API layer, which is not needed. + params.redirect_uri = providerInfo.getRedirectURI(userContext) + } + + if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { + params.actual_redirect_uri = providerInfo.authorisationRedirect.url + + Object.keys(params).forEach((key) => { + if (params[key] === providerInfo.getClientId(userContext)) + params[key] = getActualClientIdFromDevelopmentClientId(providerInfo.getClientId(userContext)) + }) + } + + const paramsString = new URLSearchParams(params).toString() + + let url = `${providerInfo.authorisationRedirect.url}?${paramsString}` + + if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) + url = `${DEV_OAUTH_AUTHORIZATION_URL}?${paramsString}` + + return { + status: 'OK', + url, + } + }, + async signInUpPOST({ + provider, + code, + redirectURI, + authCodeResponse, + options, + userContext, + }: { + provider: TypeProvider + code: string + redirectURI: string + authCodeResponse?: any + clientId?: string + options: APIOptions + userContext: any + }): Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + session: SessionContainerInterface + authCodeResponse: any + } + | { status: 'NO_EMAIL_GIVEN_BY_PROVIDER' } + | GeneralErrorResponse + > { + let accessTokenAPIResponse: any + + { + const providerInfo = provider.get(undefined, undefined, userContext) + if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { + redirectURI = DEV_OAUTH_REDIRECT_URL + } + else if (providerInfo.getRedirectURI !== undefined) { + // we overwrite the redirectURI provided by the frontend + // since the backend wants to take charge of setting this. + redirectURI = providerInfo.getRedirectURI(userContext) + } + } + + const providerInfo = provider.get(redirectURI, code, userContext) + + if (authCodeResponse !== undefined) { + accessTokenAPIResponse = { + data: authCodeResponse, + } + } + else { + // we should use code to get the authCodeResponse body + if (isUsingDevelopmentClientId(providerInfo.getClientId(userContext))) { + Object.keys(providerInfo.accessTokenAPI.params).forEach((key) => { + if (providerInfo.accessTokenAPI.params[key] === providerInfo.getClientId(userContext)) { + providerInfo.accessTokenAPI.params[key] = getActualClientIdFromDevelopmentClientId( + providerInfo.getClientId(userContext), + ) + } + }) + } + + accessTokenAPIResponse = await axios.default({ + method: 'post', + url: providerInfo.accessTokenAPI.url, + data: qs.stringify(providerInfo.accessTokenAPI.params), + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'accept': 'application/json', // few providers like github don't send back json response by default + }, + }) + } + + const userInfo = await providerInfo.getProfileInfo(accessTokenAPIResponse.data, userContext) + + const emailInfo = userInfo.email + if (emailInfo === undefined) { + return { + status: 'NO_EMAIL_GIVEN_BY_PROVIDER', + } + } + const response = await options.recipeImplementation.signInUp({ + thirdPartyId: provider.id, + thirdPartyUserId: userInfo.id, + email: emailInfo.id, + userContext, + }) + + // we set the email as verified if already verified by the OAuth provider. + // This block was added because of https://github.com/supertokens/supertokens-core/issues/295 + if (emailInfo.isVerified) { + const emailVerificationInstance = EmailVerification.getInstance() + if (emailVerificationInstance) { + const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( + { + userId: response.user.id, + email: response.user.email, + userContext, + }, + ) + + if (tokenResponse.status === 'OK') { + await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ + token: tokenResponse.token, + userContext, + }) + } + } + } + + const session = await Session.createNewSession( + options.req, + options.res, + response.user.id, + {}, + {}, + userContext, + ) + return { + status: 'OK', + createdNewUser: response.createdNewUser, + user: response.user, + session, + authCodeResponse: accessTokenAPIResponse.data, + } + }, + + async appleRedirectHandlerPOST({ code, state, options }): Promise { + const redirectURL + = `${options.appInfo.websiteDomain.getAsStringDangerous() + + options.appInfo.websiteBasePath.getAsStringDangerous() + }/callback/apple?state=${ + state + }&code=${ + code}` + options.res.sendHTMLResponse( + ``, + ) + }, + } +} + +function isUsingDevelopmentClientId(client_id: string): boolean { + return client_id.startsWith(DEV_KEY_IDENTIFIER) || DEV_OAUTH_CLIENT_IDS.includes(client_id) +} + +export function getActualClientIdFromDevelopmentClientId(client_id: string): string { + if (client_id.startsWith(DEV_KEY_IDENTIFIER)) + return client_id.split(DEV_KEY_IDENTIFIER)[1] + + return client_id +} diff --git a/src/recipe/thirdparty/api/signinup.ts b/src/recipe/thirdparty/api/signinup.ts new file mode 100644 index 000000000..c0e25f87c --- /dev/null +++ b/src/recipe/thirdparty/api/signinup.ts @@ -0,0 +1,112 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../error' +import { makeDefaultUserContextFromAPI, send200Response } from '../../../utils' +import { APIInterface, APIOptions } from '../' +import { findRightProvider } from '../utils' + +export default async function signInUpAPI(apiImplementation: APIInterface, options: APIOptions): Promise { + if (apiImplementation.signInUpPOST === undefined) + return false + + const bodyParams = await options.req.getJSONBody() + const thirdPartyId = bodyParams.thirdPartyId + const code = bodyParams.code === undefined ? '' : bodyParams.code + const redirectURI = bodyParams.redirectURI + const authCodeResponse = bodyParams.authCodeResponse + const clientId = bodyParams.clientId + + if (thirdPartyId === undefined || typeof thirdPartyId !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the thirdPartyId in request body', + }) + } + + if (typeof code !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please make sure that the code in the request body is a string', + }) + } + + if (code === '' && authCodeResponse === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide one of code or authCodeResponse in the request body', + }) + } + + if (authCodeResponse !== undefined && authCodeResponse.access_token === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the access_token inside the authCodeResponse request param', + }) + } + + if (redirectURI === undefined || typeof redirectURI !== 'string') { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: 'Please provide the redirectURI in request body', + }) + } + + const provider = findRightProvider(options.providers, thirdPartyId, clientId) + if (provider === undefined) { + if (clientId === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: `The third party provider ${thirdPartyId} seems to be missing from the backend configs.`, + }) + } + else { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: + `The third party provider ${ + thirdPartyId + } seems to be missing from the backend configs. If it is configured, then please make sure that you are passing the correct clientId from the frontend.`, + }) + } + } + + const result = await apiImplementation.signInUpPOST({ + provider, + code, + clientId, + redirectURI, + options, + authCodeResponse, + userContext: makeDefaultUserContextFromAPI(options.req), + }) + + if (result.status === 'OK') { + send200Response(options.res, { + status: result.status, + user: result.user, + createdNewUser: result.createdNewUser, + }) + } + else if (result.status === 'NO_EMAIL_GIVEN_BY_PROVIDER') { + send200Response(options.res, { + status: 'NO_EMAIL_GIVEN_BY_PROVIDER', + }) + } + else { + send200Response(options.res, result) + } + return true +} diff --git a/src/recipe/thirdparty/constants.ts b/src/recipe/thirdparty/constants.ts new file mode 100644 index 000000000..9154becb5 --- /dev/null +++ b/src/recipe/thirdparty/constants.ts @@ -0,0 +1,20 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +export const AUTHORISATION_API = '/authorisationurl' + +export const SIGN_IN_UP_API = '/signinup' + +export const APPLE_REDIRECT_HANDLER = '/callback/apple' diff --git a/src/recipe/thirdparty/error.ts b/src/recipe/thirdparty/error.ts new file mode 100644 index 000000000..43e079c3f --- /dev/null +++ b/src/recipe/thirdparty/error.ts @@ -0,0 +1,25 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../../error' + +export default class ThirdPartyError extends STError { + constructor(options: { type: 'BAD_INPUT_ERROR'; message: string }) { + super({ + ...options, + }) + this.fromRecipe = 'thirdparty' + } +} diff --git a/src/recipe/thirdparty/index.ts b/src/recipe/thirdparty/index.ts new file mode 100644 index 000000000..056f96e9c --- /dev/null +++ b/src/recipe/thirdparty/index.ts @@ -0,0 +1,104 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import Recipe from './recipe' +import SuperTokensError from './error' +import * as thirdPartyProviders from './providers' +import { APIInterface, APIOptions, RecipeInterface, TypeProvider, User } from './types' + +export default class Wrapper { + static init = Recipe.init + + static Error = SuperTokensError + + static async signInUp(thirdPartyId: string, thirdPartyUserId: string, email: string, userContext: any = {}) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signInUp({ + thirdPartyId, + thirdPartyUserId, + email, + userContext, + }) + } + + static getUserById(userId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }) + } + + static getUsersByEmail(email: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }) + } + + static getUserByThirdPartyInfo(thirdPartyId: string, thirdPartyUserId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ + thirdPartyId, + thirdPartyUserId, + userContext, + }) + } + + static Google = thirdPartyProviders.Google + + static Github = thirdPartyProviders.Github + + static Facebook = thirdPartyProviders.Facebook + + static Apple = thirdPartyProviders.Apple + + static Discord = thirdPartyProviders.Discord + + static GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces + + static Bitbucket = thirdPartyProviders.Bitbucket + + static GitLab = thirdPartyProviders.GitLab + + // static Okta = thirdPartyProviders.Okta; + + // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; +} + +export const init = Wrapper.init + +export const Error = Wrapper.Error + +export const signInUp = Wrapper.signInUp + +export const getUserById = Wrapper.getUserById + +export const getUsersByEmail = Wrapper.getUsersByEmail + +export const getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo + +export const Google = Wrapper.Google + +export const Github = Wrapper.Github + +export const Facebook = Wrapper.Facebook + +export const Apple = Wrapper.Apple + +export const Discord = Wrapper.Discord + +export const GoogleWorkspaces = Wrapper.GoogleWorkspaces + +export const Bitbucket = Wrapper.Bitbucket + +export const GitLab = Wrapper.GitLab + +// export let Okta = Wrapper.Okta; + +// export let ActiveDirectory = Wrapper.ActiveDirectory; + +export type { RecipeInterface, User, APIInterface, APIOptions, TypeProvider } diff --git a/lib/ts/recipe/thirdparty/providers/activeDirectory.ts b/src/recipe/thirdparty/providers/activeDirectory.ts similarity index 100% rename from lib/ts/recipe/thirdparty/providers/activeDirectory.ts rename to src/recipe/thirdparty/providers/activeDirectory.ts diff --git a/src/recipe/thirdparty/providers/apple.ts b/src/recipe/thirdparty/providers/apple.ts new file mode 100644 index 000000000..709e581e5 --- /dev/null +++ b/src/recipe/thirdparty/providers/apple.ts @@ -0,0 +1,171 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { sign as jwtSign } from 'jsonwebtoken' +import verifyAppleToken from 'verify-apple-id-token' +import { TypeProvider, TypeProviderGetResponse } from '../types' +import STError from '../error' +import { getActualClientIdFromDevelopmentClientId } from '../api/implementation' +import SuperTokens from '../../../supertokens' +import { APPLE_REDIRECT_HANDLER } from '../constants' + +interface TypeThirdPartyProviderAppleConfig { + clientId: string + clientSecret: { + keyId: string + privateKey: string + teamId: string + } + scope?: string[] + authorisationRedirect?: { + params?: { [key: string]: string | ((request: any) => string) } + } + isDefault?: boolean +} + +export default function Apple(config: TypeThirdPartyProviderAppleConfig): TypeProvider { + const id = 'apple' + + function getClientSecret(clientId: string, keyId: string, teamId: string, privateKey: string): string { + return jwtSign( + { + iss: teamId, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months + aud: 'https://appleid.apple.com', + sub: getActualClientIdFromDevelopmentClientId(clientId), + }, + privateKey.replace(/\\n/g, '\n'), + { algorithm: 'ES256', keyid: keyId }, + ) + } + try { + // trying to generate a client secret, in case client has not passed the values correctly + getClientSecret( + config.clientId, + config.clientSecret.keyId, + config.clientSecret.teamId, + config.clientSecret.privateKey, + ) + } + catch (error: any) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: error.message, + }) + } + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const accessTokenAPIURL = 'https://appleid.apple.com/auth/token' + const clientSecret = getClientSecret( + config.clientId, + config.clientSecret.keyId, + config.clientSecret.teamId, + config.clientSecret.privateKey, + ) + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: clientSecret, + grant_type: 'authorization_code', + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = 'https://appleid.apple.com/auth/authorize' + let scopes: string[] = ['email'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const additionalParams + = (config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined) + ? {} + : config.authorisationRedirect.params + + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + response_mode: 'form_post', + response_type: 'code', + client_id: config.clientId, + ...additionalParams, + } + + async function getProfileInfo(accessTokenAPIResponse: { + access_token: string + expires_in: number + token_type: string + refresh_token: string + id_token: string + }) { + /* + - Verify the JWS E256 signature using the server’s public key + - Verify the nonce for the authentication + - Verify that the iss field contains https://appleid.apple.com + - Verify that the aud field is the developer’s client_id + - Verify that the time is earlier than the exp value of the token */ + const payload = await verifyAppleToken({ + idToken: accessTokenAPIResponse.id_token, + clientId: getActualClientIdFromDevelopmentClientId(config.clientId), + }) + if (payload === null) + throw new Error('no user info found from user\'s id token received from apple') + + const id = (payload as any).sub as string + const email = (payload as any).email as string + const isVerified = (payload as any).email_verified + if (id === undefined || id === null) + throw new Error('no user info found from user\'s id token received from apple') + + return { + id, + email: { + id: email, + isVerified, + }, + } + } + function getRedirectURI() { + const supertokens = SuperTokens.getInstanceOrThrowError() + return ( + supertokens.appInfo.apiDomain.getAsStringDangerous() + + supertokens.appInfo.apiBasePath.getAsStringDangerous() + + APPLE_REDIRECT_HANDLER + ) + } + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + getRedirectURI, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/bitbucket.ts b/src/recipe/thirdparty/providers/bitbucket.ts new file mode 100644 index 000000000..d4c6a7591 --- /dev/null +++ b/src/recipe/thirdparty/providers/bitbucket.ts @@ -0,0 +1,133 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import axios from 'axios' +import { TypeProvider, TypeProviderGetResponse } from '../types' + +interface TypeThirdPartyProviderBitbucketConfig { + clientId: string + clientSecret: string + scope?: string[] + authorisationRedirect?: { + params?: { [key: string]: string | ((request: any) => string) } + } + isDefault?: boolean +} + +export default function Bitbucket(config: TypeThirdPartyProviderBitbucketConfig): TypeProvider { + const id = 'bitbucket' + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const accessTokenAPIURL = 'https://bitbucket.org/site/oauth2/access_token' + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = 'https://bitbucket.org/site/oauth2/authorize' + let scopes = ['account', 'email'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const additionalParams + = (config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined) + ? {} + : config.authorisationRedirect.params + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + access_type: 'offline', + response_type: 'code', + client_id: config.clientId, + ...additionalParams, + } + + async function getProfileInfo(accessTokenAPIResponse: { + access_token: string + expires_in: number + token_type: string + refresh_token?: string + }) { + const accessToken = accessTokenAPIResponse.access_token + const authHeader = `Bearer ${accessToken}` + const response = await axios({ + method: 'get', + url: 'https://api.bitbucket.org/2.0/user', + headers: { + Authorization: authHeader, + }, + }) + const userInfo = response.data + const id = userInfo.uuid + + const emailRes = await axios({ + method: 'get', + url: 'https://api.bitbucket.org/2.0/user/emails', + headers: { + Authorization: authHeader, + }, + }) + const emailData = emailRes.data + let email + let isVerified = false + emailData.values.forEach((emailInfo: any) => { + if (emailInfo.is_primary) { + email = emailInfo.email + isVerified = emailInfo.is_confirmed + } + }) + + if (email === undefined) { + return { + id, + } + } + return { + id, + email: { + id: email, + isVerified, + }, + } + } + + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/discord.ts b/src/recipe/thirdparty/providers/discord.ts new file mode 100644 index 000000000..c2bacd47a --- /dev/null +++ b/src/recipe/thirdparty/providers/discord.ts @@ -0,0 +1,108 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios from 'axios' +import { TypeProvider, TypeProviderGetResponse } from '../types' + +interface TypeThirdPartyProviderDiscordConfig { + clientId: string + clientSecret: string + scope?: string[] + authorisationRedirect?: { + params?: { [key: string]: string | ((request: any) => string) } + } + isDefault?: boolean +} + +export default function Discord(config: TypeThirdPartyProviderDiscordConfig): TypeProvider { + const id = 'discord' + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const accessTokenAPIURL = 'https://discord.com/api/oauth2/token' + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = 'https://discord.com/api/oauth2/authorize' + let scopes = ['email', 'identify'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const additionalParams + = (config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined) + ? {} + : config.authorisationRedirect.params + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + client_id: config.clientId, + response_type: 'code', + ...additionalParams, + } + + async function getProfileInfo(accessTokenAPIResponse: { + access_token: string + expires_in: number + token_type: string + }) { + const accessToken = accessTokenAPIResponse.access_token + const authHeader = `Bearer ${accessToken}` + const response = await axios({ + method: 'get', + url: 'https://discord.com/api/users/@me', + headers: { + Authorization: authHeader, + }, + }) + const userInfo = response.data + return { + id: userInfo.id, + email: + userInfo.email === undefined + ? undefined + : { + id: userInfo.email, + isVerified: userInfo.verified, + }, + } + } + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/facebook.ts b/src/recipe/thirdparty/providers/facebook.ts new file mode 100644 index 000000000..1f5a37cf1 --- /dev/null +++ b/src/recipe/thirdparty/providers/facebook.ts @@ -0,0 +1,104 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios from 'axios' +import { TypeProvider, TypeProviderGetResponse } from '../types' + +interface TypeThirdPartyProviderFacebookConfig { + clientId: string + clientSecret: string + scope?: string[] + isDefault?: boolean +} + +export default function Facebook(config: TypeThirdPartyProviderFacebookConfig): TypeProvider { + const id = 'facebook' + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const accessTokenAPIURL = 'https://graph.facebook.com/v9.0/oauth/access_token' + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: config.clientSecret, + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = 'https://www.facebook.com/v9.0/dialog/oauth' + let scopes = ['email'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + response_type: 'code', + client_id: config.clientId, + } + + async function getProfileInfo(accessTokenAPIResponse: { + access_token: string + expires_in: number + token_type: string + }) { + const accessToken = accessTokenAPIResponse.access_token + const response = await axios({ + method: 'get', + url: 'https://graph.facebook.com/me', + params: { + access_token: accessToken, + fields: 'id,email', + format: 'json', + }, + }) + const userInfo = response.data + const id = userInfo.id + const email = userInfo.email + if (email === undefined || email === null) { + return { + id, + } + } + return { + id, + email: { + id: email, + isVerified: true, + }, + } + } + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/github.ts b/src/recipe/thirdparty/providers/github.ts new file mode 100644 index 000000000..b11babf3e --- /dev/null +++ b/src/recipe/thirdparty/providers/github.ts @@ -0,0 +1,138 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios from 'axios' +import { TypeProvider, TypeProviderGetResponse } from '../types' + +interface TypeThirdPartyProviderGithubConfig { + clientId: string + clientSecret: string + scope?: string[] + authorisationRedirect?: { + params?: { [key: string]: string | ((request: any) => string) } + } + isDefault?: boolean +} + +export default function Github(config: TypeThirdPartyProviderGithubConfig): TypeProvider { + const id = 'github' + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const accessTokenAPIURL = 'https://github.com/login/oauth/access_token' + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: config.clientSecret, + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = 'https://github.com/login/oauth/authorize' + let scopes = ['read:user', 'user:email'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const additionalParams + = (config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined) + ? {} + : config.authorisationRedirect.params + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + client_id: config.clientId, + ...additionalParams, + } + + async function getProfileInfo(accessTokenAPIResponse: { + access_token: string + expires_in: number + token_type: string + }) { + const accessToken = accessTokenAPIResponse.access_token + const authHeader = `Bearer ${accessToken}` + const response = await axios({ + method: 'get', + url: 'https://api.github.com/user', + headers: { + Authorization: authHeader, + Accept: 'application/vnd.github.v3+json', + }, + }) + const emailsInfoResponse = await axios({ + url: 'https://api.github.com/user/emails', + headers: { + Authorization: authHeader, + Accept: 'application/vnd.github.v3+json', + }, + }) + const userInfo = response.data + const emailsInfo = emailsInfoResponse.data + const id = userInfo.id.toString() // github userId will be a number + /* + if user has choosen not to show their email publicly, userInfo here will + have email as null. So we instead get the info from the emails api and + use the email which is marked as primary one. + + Sample github response for email info + [ + { + email: '', + primary: true, + verified: true, + visibility: 'public' + } + ] + */ + const emailInfo = emailsInfo.find((e: any) => e.primary) + if (emailInfo === undefined) { + return { + id, + } + } + const isVerified = emailInfo !== undefined ? emailInfo.verified : false + return { + id, + email: + emailInfo.email === undefined + ? undefined + : { + id: emailInfo.email, + isVerified, + }, + } + } + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/gitlab.ts b/src/recipe/thirdparty/providers/gitlab.ts new file mode 100644 index 000000000..a3af8e1bb --- /dev/null +++ b/src/recipe/thirdparty/providers/gitlab.ts @@ -0,0 +1,122 @@ +/* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import axios from 'axios' +import { TypeProvider, TypeProviderGetResponse } from '../types' +import NormalisedURLDomain from '../../../normalisedURLDomain' + +interface TypeThirdPartyProviderGitLabConfig { + clientId: string + clientSecret: string + scope?: string[] + authorisationRedirect?: { + params?: { [key: string]: string | ((request: any) => string) } + } + gitlabBaseUrl?: string + isDefault?: boolean +} + +export default function GitLab(config: TypeThirdPartyProviderGitLabConfig): TypeProvider { + const id = 'gitlab' + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const baseUrl + = config.gitlabBaseUrl === undefined + ? 'https://gitlab.com' // no traling slash cause we add that in the path + : new NormalisedURLDomain(config.gitlabBaseUrl).getAsStringDangerous() + const accessTokenAPIURL = `${baseUrl}/oauth/token` + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = `${baseUrl}/oauth/authorize` + let scopes = ['read_user'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const additionalParams + = (config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined) + ? {} + : config.authorisationRedirect.params + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + response_type: 'code', + client_id: config.clientId, + ...additionalParams, + } + + async function getProfileInfo(accessTokenAPIResponse: { + access_token: string + expires_in: number + token_type: string + refresh_token?: string + }) { + const accessToken = accessTokenAPIResponse.access_token + const authHeader = `Bearer ${accessToken}` + const response = await axios({ + method: 'get', + url: `${baseUrl}/api/v4/user`, + headers: { + Authorization: authHeader, + }, + }) + const userInfo = response.data + const id = `${userInfo.id}` + const email = userInfo.email + if (email === undefined || email === null) { + return { + id, + } + } + const isVerified = userInfo.confirmed_at !== null && userInfo.confirmed_at !== undefined + return { + id, + email: { + id: email, + isVerified, + }, + } + } + + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/google.ts b/src/recipe/thirdparty/providers/google.ts new file mode 100644 index 000000000..17de97ae9 --- /dev/null +++ b/src/recipe/thirdparty/providers/google.ts @@ -0,0 +1,120 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import axios from 'axios' +import { TypeProvider, TypeProviderGetResponse } from '../types' + +interface TypeThirdPartyProviderGoogleConfig { + clientId: string + clientSecret: string + scope?: string[] + authorisationRedirect?: { + params?: { [key: string]: string | ((request: any) => string) } + } + isDefault?: boolean +} + +export default function Google(config: TypeThirdPartyProviderGoogleConfig): TypeProvider { + const id = 'google' + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const accessTokenAPIURL = 'https://oauth2.googleapis.com/token' + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = 'https://accounts.google.com/o/oauth2/v2/auth' + let scopes = ['https://www.googleapis.com/auth/userinfo.email'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const additionalParams + = (config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined) + ? {} + : config.authorisationRedirect.params + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + client_id: config.clientId, + ...additionalParams, + } + + async function getProfileInfo(accessTokenAPIResponse: { + access_token: string + expires_in: number + token_type: string + scope: string + refresh_token: string + }) { + const accessToken = accessTokenAPIResponse.access_token + const authHeader = `Bearer ${accessToken}` + const response = await axios({ + method: 'get', + url: 'https://www.googleapis.com/oauth2/v1/userinfo', + params: { + alt: 'json', + }, + headers: { + Authorization: authHeader, + }, + }) + const userInfo = response.data + const id = userInfo.id + const email = userInfo.email + if (email === undefined || email === null) { + return { + id, + } + } + const isVerified = userInfo.verified_email + return { + id, + email: { + id: email, + isVerified, + }, + } + } + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/googleWorkspaces.ts b/src/recipe/thirdparty/providers/googleWorkspaces.ts new file mode 100644 index 000000000..3fe4c2dea --- /dev/null +++ b/src/recipe/thirdparty/providers/googleWorkspaces.ts @@ -0,0 +1,116 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeProvider, TypeProviderGetResponse } from '../types' +import { getActualClientIdFromDevelopmentClientId } from '../api/implementation' +import { verifyIdTokenFromJWKSEndpoint } from './utils' + +interface TypeThirdPartyProviderGoogleWorkspacesConfig { + clientId: string + clientSecret: string + scope?: string[] + domain?: string + authorisationRedirect?: { + params?: { [key: string]: string | ((request: any) => string) } + } + isDefault?: boolean +} + +export default function GW(config: TypeThirdPartyProviderGoogleWorkspacesConfig): TypeProvider { + const id = 'google-workspaces' + const domain: string = config.domain === undefined ? '*' : config.domain + + function get(redirectURI: string | undefined, authCodeFromRequest: string | undefined): TypeProviderGetResponse { + const accessTokenAPIURL = 'https://oauth2.googleapis.com/token' + const accessTokenAPIParams: { [key: string]: string } = { + client_id: config.clientId, + client_secret: config.clientSecret, + grant_type: 'authorization_code', + } + if (authCodeFromRequest !== undefined) + accessTokenAPIParams.code = authCodeFromRequest + + if (redirectURI !== undefined) + accessTokenAPIParams.redirect_uri = redirectURI + + const authorisationRedirectURL = 'https://accounts.google.com/o/oauth2/v2/auth' + let scopes = ['https://www.googleapis.com/auth/userinfo.email'] + if (config.scope !== undefined) { + scopes = config.scope + scopes = Array.from(new Set(scopes)) + } + const additionalParams + = (config.authorisationRedirect === undefined || config.authorisationRedirect.params === undefined) + ? {} + : config.authorisationRedirect.params + const authorizationRedirectParams: { [key: string]: string } = { + scope: scopes.join(' '), + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + client_id: config.clientId, + hd: domain, + ...additionalParams, + } + + async function getProfileInfo(authCodeResponse: { id_token: string }) { + const payload: any = await verifyIdTokenFromJWKSEndpoint( + authCodeResponse.id_token, + 'https://www.googleapis.com/oauth2/v3/certs', + { + audience: getActualClientIdFromDevelopmentClientId(config.clientId), + issuer: ['https://accounts.google.com', 'accounts.google.com'], + }, + ) + + if (payload.email === undefined) + throw new Error('Could not get email. Please use a different login method') + + if (payload.hd === undefined) + throw new Error('Please use a Google Workspace ID to login') + + // if the domain is "*" in it, it means that any workspace email is allowed. + if (!domain.includes('*') && payload.hd !== domain) + throw new Error(`Please use emails from ${domain} to login`) + + return { + id: payload.sub, + email: { + id: payload.email, + isVerified: payload.email_verified, + }, + } + } + return { + accessTokenAPI: { + url: accessTokenAPIURL, + params: accessTokenAPIParams, + }, + authorisationRedirect: { + url: authorisationRedirectURL, + params: authorizationRedirectParams, + }, + getProfileInfo, + getClientId: () => { + return config.clientId + }, + } + } + + return { + id, + get, + isDefault: config.isDefault, + } +} diff --git a/src/recipe/thirdparty/providers/index.ts b/src/recipe/thirdparty/providers/index.ts new file mode 100644 index 000000000..9a090610b --- /dev/null +++ b/src/recipe/thirdparty/providers/index.ts @@ -0,0 +1,21 @@ +import ProviderGoogle from './google' +import ProviderFacebook from './facebook' +import ProviderGithub from './github' +import ProviderApple from './apple' +import ProviderDiscord from './discord' +// import ProviderOkta from "./okta"; +import ProviderGoogleWorkspaces from './googleWorkspaces' +// import ProviderAD from "./activeDirectory"; +import ProviderBitbucket from './bitbucket' +import ProviderGitlab from './gitlab' + +export const Google = ProviderGoogle +export const Facebook = ProviderFacebook +export const Github = ProviderGithub +export const Apple = ProviderApple +export const Discord = ProviderDiscord +export const GoogleWorkspaces = ProviderGoogleWorkspaces +export const Bitbucket = ProviderBitbucket +export const GitLab = ProviderGitlab +// export let Okta = ProviderOkta; +// export let ActiveDirectory = ProviderAD; diff --git a/lib/ts/recipe/thirdparty/providers/okta.ts b/src/recipe/thirdparty/providers/okta.ts similarity index 100% rename from lib/ts/recipe/thirdparty/providers/okta.ts rename to src/recipe/thirdparty/providers/okta.ts diff --git a/src/recipe/thirdparty/providers/utils.ts b/src/recipe/thirdparty/providers/utils.ts new file mode 100644 index 000000000..ba8133214 --- /dev/null +++ b/src/recipe/thirdparty/providers/utils.ts @@ -0,0 +1,29 @@ +import { VerifyOptions, verify } from 'jsonwebtoken' +import jwksClient from 'jwks-rsa' + +export async function verifyIdTokenFromJWKSEndpoint( + idToken: string, + jwksUri: string, + otherOptions: VerifyOptions, +): Promise { + const client = jwksClient({ + jwksUri, + }) + function getKey(header: any, callback: any) { + client.getSigningKey(header.kid, (_, key: any) => { + const signingKey = key.publicKey || key.rsaPublicKey + callback(null, signingKey) + }) + } + + const payload: any = await new Promise((resolve, reject) => { + verify(idToken, getKey, otherOptions, (err, decoded) => { + if (err) + reject(err) + else + resolve(decoded) + }) + }) + + return payload +} diff --git a/src/recipe/thirdparty/recipe.ts b/src/recipe/thirdparty/recipe.ts new file mode 100644 index 000000000..e25241144 --- /dev/null +++ b/src/recipe/thirdparty/recipe.ts @@ -0,0 +1,191 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import EmailVerificationRecipe from '../emailverification/recipe' +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { PostSuperTokensInitCallbacks } from '../../postSuperTokensInitCallbacks' +import { GetEmailForUserIdFunc } from '../emailverification/types' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput, TypeProvider } from './types' +import { validateAndNormaliseUserInput } from './utils' +import STError from './error' + +import { APPLE_REDIRECT_HANDLER, AUTHORISATION_API, SIGN_IN_UP_API } from './constants' +import signInUpAPI from './api/signinup' +import authorisationUrlAPI from './api/authorisationUrl' +import RecipeImplementation from './recipeImplementation' +import APIImplementation from './api/implementation' +import appleRedirectHandler from './api/appleRedirect' + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined + static RECIPE_ID = 'thirdparty' + + config: TypeNormalisedInput + + providers: TypeProvider[] + + recipeInterfaceImpl: RecipeInterface + + apiImpl: APIInterface + + isInServerlessEnv: boolean + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + _recipes: {}, + _ingredients: {}, + ) { + super(recipeId, appInfo) + this.config = validateAndNormaliseUserInput(appInfo, config) + this.isInServerlessEnv = isInServerlessEnv + + this.providers = this.config.signInAndUpFeature.providers + + { + const builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const emailVerificationRecipe = EmailVerificationRecipe.getInstance() + if (emailVerificationRecipe !== undefined) + emailVerificationRecipe.addGetEmailForUserIdFunc(this.getEmailForUserId.bind(this)) + }) + } + + static init(config: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe( + Recipe.RECIPE_ID, + appInfo, + isInServerlessEnv, + config, + {}, + { + emailDelivery: undefined, + }, + ) + return Recipe.instance + } + else { + throw new Error('ThirdParty recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + getAPIsHandled = (): APIHandled[] => { + return [ + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(SIGN_IN_UP_API), + id: SIGN_IN_UP_API, + disabled: this.apiImpl.signInUpPOST === undefined, + }, + { + method: 'get', + pathWithoutApiBasePath: new NormalisedURLPath(AUTHORISATION_API), + id: AUTHORISATION_API, + disabled: this.apiImpl.authorisationUrlGET === undefined, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath(APPLE_REDIRECT_HANDLER), + id: APPLE_REDIRECT_HANDLER, + disabled: this.apiImpl.appleRedirectHandlerPOST === undefined, + }, + ] + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + ): Promise => { + const options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + providers: this.providers, + req, + res, + appInfo: this.getAppInfo(), + } + if (id === SIGN_IN_UP_API) + return await signInUpAPI(this.apiImpl, options) + else if (id === AUTHORISATION_API) + return await authorisationUrlAPI(this.apiImpl, options) + else if (id === APPLE_REDIRECT_HANDLER) + return await appleRedirectHandler(this.apiImpl, options) + + return false + } + + handleError = async (err: STError, _request: BaseRequest, _response: BaseResponse): Promise => { + throw err + } + + getAllCORSHeaders = (): string[] => { + return [] + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } + + // helper functions... + getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { + const userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }) + if (userInfo !== undefined) { + return { + status: 'OK', + email: userInfo.email, + } + } + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } +} diff --git a/src/recipe/thirdparty/recipeImplementation.ts b/src/recipe/thirdparty/recipeImplementation.ts new file mode 100644 index 000000000..402a96d9c --- /dev/null +++ b/src/recipe/thirdparty/recipeImplementation.ts @@ -0,0 +1,71 @@ +import { Querier } from '../../querier' +import NormalisedURLPath from '../../normalisedURLPath' +import { RecipeInterface, User } from './types' + +export default function getRecipeImplementation(querier: Querier): RecipeInterface { + return { + async signInUp({ + thirdPartyId, + thirdPartyUserId, + email, + }: { + thirdPartyId: string + thirdPartyUserId: string + email: string + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> { + const response = await querier.sendPostRequest(new NormalisedURLPath('/recipe/signinup'), { + thirdPartyId, + thirdPartyUserId, + email: { id: email }, + }) + return { + status: 'OK', + createdNewUser: response.createdNewUser, + user: response.user, + } + }, + + async getUserById({ userId }: { userId: string }): Promise { + const response = await querier.sendGetRequest(new NormalisedURLPath('/recipe/user'), { + userId, + }) + if (response.status === 'OK') { + return { + ...response.user, + } + } + else { + return undefined + } + }, + + async getUsersByEmail({ email }: { email: string }): Promise { + const { users } = await querier.sendGetRequest(new NormalisedURLPath('/recipe/users/by-email'), { + email, + }) + + return users + }, + + async getUserByThirdPartyInfo({ + thirdPartyId, + thirdPartyUserId, + }: { + thirdPartyId: string + thirdPartyUserId: string + }): Promise { + const response = await querier.sendGetRequest(new NormalisedURLPath('/recipe/user'), { + thirdPartyId, + thirdPartyUserId, + }) + if (response.status === 'OK') { + return { + ...response.user, + } + } + else { + return undefined + } + }, + } +} diff --git a/src/recipe/thirdparty/types.ts b/src/recipe/thirdparty/types.ts new file mode 100644 index 000000000..5588abcad --- /dev/null +++ b/src/recipe/thirdparty/types.ts @@ -0,0 +1,160 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import { GeneralErrorResponse, NormalisedAppinfo } from '../../types' +import { SessionContainerInterface } from '../session/types' + +export interface UserInfo { id: string; email?: { id: string; isVerified: boolean } } + +export interface TypeProviderGetResponse { + accessTokenAPI: { + url: string + params: { [key: string]: string } // Will be merged with our object + } + authorisationRedirect: { + url: string + params: { [key: string]: string | ((request: any) => string) } + } + getProfileInfo: (authCodeResponse: any, userContext: any) => Promise + getClientId: (userContext: any) => string + getRedirectURI?: (userContext: any) => string // if undefined, the redirect_uri is set on the frontend. +} + +export interface TypeProvider { + id: string + get: ( + redirectURI: string | undefined, + authCodeFromRequest: string | undefined, + userContext: any + ) => TypeProviderGetResponse + isDefault?: boolean // if not present, we treat it as false +} + +export interface User { + // https://github.com/supertokens/core-driver-interface/wiki#third-party-user + id: string + timeJoined: number + email: string + thirdParty: { + id: string + userId: string + } +} + +export interface TypeInputSignInAndUp { + providers: TypeProvider[] +} + +export interface TypeNormalisedInputSignInAndUp { + providers: TypeProvider[] +} + +export interface TypeInput { + signInAndUpFeature: TypeInputSignInAndUp + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeNormalisedInput { + signInAndUpFeature: TypeNormalisedInputSignInAndUp + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface RecipeInterface { + getUserById(input: { userId: string; userContext: any }): Promise + + getUsersByEmail(input: { email: string; userContext: any }): Promise + + getUserByThirdPartyInfo(input: { + thirdPartyId: string + thirdPartyUserId: string + userContext: any + }): Promise + + signInUp(input: { + thirdPartyId: string + thirdPartyUserId: string + email: string + userContext: any + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> +} + +export interface APIOptions { + recipeImplementation: RecipeInterface + config: TypeNormalisedInput + recipeId: string + isInServerlessEnv: boolean + providers: TypeProvider[] + req: BaseRequest + res: BaseResponse + appInfo: NormalisedAppinfo +} + +export interface APIInterface { + authorisationUrlGET: + | undefined + | ((input: { + provider: TypeProvider + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + url: string + } + | GeneralErrorResponse + >) + + signInUpPOST: + | undefined + | ((input: { + provider: TypeProvider + code: string + redirectURI: string + authCodeResponse?: any + clientId?: string + options: APIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + session: SessionContainerInterface + authCodeResponse: any + } + | { status: 'NO_EMAIL_GIVEN_BY_PROVIDER' } + | GeneralErrorResponse + >) + + appleRedirectHandlerPOST: + | undefined + | ((input: { code: string; state: string; options: APIOptions; userContext: any }) => Promise) + +} diff --git a/src/recipe/thirdparty/utils.ts b/src/recipe/thirdparty/utils.ts new file mode 100644 index 000000000..6ca5ee5f5 --- /dev/null +++ b/src/recipe/thirdparty/utils.ts @@ -0,0 +1,109 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import { APIInterface, RecipeInterface, TypeInput, TypeInputSignInAndUp, TypeNormalisedInput, TypeNormalisedInputSignInAndUp, TypeProvider } from './types' + +export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { + const signInAndUpFeature = validateAndNormaliseSignInAndUpConfig(appInfo, config.signInAndUpFeature) + + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config.override, + } + + return { + signInAndUpFeature, + override, + } +} + +export function findRightProvider( + providers: TypeProvider[], + thirdPartyId: string, + clientId?: string, +): TypeProvider | undefined { + return providers.find((p) => { + const id = p.id + if (id !== thirdPartyId) + return false + + // first if there is only one provider with thirdPartyId in the providers array, + const otherProvidersWithSameId = providers.filter(p1 => p1.id === id && p !== p1) + if (otherProvidersWithSameId.length === 0) { + // they we always return that. + return true + } + + // otherwise, we look for the isDefault provider if clientId is missing + if (clientId === undefined) + return p.isDefault === true + + // otherwise, we return a provider that matches based on client ID as well. + return p.get(undefined, undefined, {}).getClientId({}) === clientId + }) +} + +function validateAndNormaliseSignInAndUpConfig( + _: NormalisedAppinfo, + config: TypeInputSignInAndUp, +): TypeNormalisedInputSignInAndUp { + const providers = config.providers + + if (providers === undefined || providers.length === 0) { + throw new Error( + 'thirdparty recipe requires atleast 1 provider to be passed in signInAndUpFeature.providers config', + ) + } + + // we check if there are multiple providers with the same id that have isDefault as true. + // In this case, we want to throw an error.. + const isDefaultProvidersSet = new Set() + const allProvidersSet = new Set() + providers.forEach((p) => { + const id = p.id + allProvidersSet.add(p.id) + let isDefault = p.isDefault + + if (isDefault === undefined) { + // if this id is not being used by any other provider, we treat this as the isDefault + const otherProvidersWithSameId = providers.filter(p1 => p1.id === id && p !== p1) + if (otherProvidersWithSameId.length === 0) { + // we treat this as the isDefault now... + isDefault = true + } + } + if (isDefault) { + if (isDefaultProvidersSet.has(id)) { + throw new Error( + `You have provided multiple third party providers that have the id: "${id}" and are marked as "isDefault: true". Please only mark one of them as isDefault.`, + ) + } + isDefaultProvidersSet.add(id) + } + }) + + if (isDefaultProvidersSet.size !== allProvidersSet.size) { + // this means that there is no provider marked as isDefault + throw new Error( + 'The providers array has multiple entries for the same third party provider. Please mark one of them as the default one by using "isDefault: true".', + ) + } + + return { + providers, + } +} diff --git a/src/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.ts b/src/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.ts new file mode 100644 index 000000000..ec6483c5f --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/api/emailPasswordAPIImplementation.ts @@ -0,0 +1,12 @@ +import { APIInterface } from '../../emailpassword' +import { APIInterface as ThirdPartyEmailPasswordAPIInterface } from '../' + +export default function getIterfaceImpl(apiImplmentation: ThirdPartyEmailPasswordAPIInterface): APIInterface { + return { + emailExistsGET: apiImplmentation.emailPasswordEmailExistsGET?.bind(apiImplmentation), + generatePasswordResetTokenPOST: apiImplmentation.generatePasswordResetTokenPOST?.bind(apiImplmentation), + passwordResetPOST: apiImplmentation.passwordResetPOST?.bind(apiImplmentation), + signInPOST: apiImplmentation.emailPasswordSignInPOST?.bind(apiImplmentation), + signUpPOST: apiImplmentation.emailPasswordSignUpPOST?.bind(apiImplmentation), + } +} diff --git a/src/recipe/thirdpartyemailpassword/api/implementation.ts b/src/recipe/thirdpartyemailpassword/api/implementation.ts new file mode 100644 index 000000000..5ee3088f0 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/api/implementation.ts @@ -0,0 +1,22 @@ +import { APIInterface } from '../' +import EmailPasswordAPIImplementation from '../../emailpassword/api/implementation' +import ThirdPartyAPIImplementation from '../../thirdparty/api/implementation' +import DerivedEP from './emailPasswordAPIImplementation' +import DerivedTP from './thirdPartyAPIImplementation' + +export default function getAPIImplementation(): APIInterface { + const emailPasswordImplementation = EmailPasswordAPIImplementation() + const thirdPartyImplementation = ThirdPartyAPIImplementation() + return { + emailPasswordEmailExistsGET: emailPasswordImplementation.emailExistsGET?.bind(DerivedEP(this)), + authorisationUrlGET: thirdPartyImplementation.authorisationUrlGET?.bind(DerivedTP(this)), + emailPasswordSignInPOST: emailPasswordImplementation.signInPOST?.bind(DerivedEP(this)), + emailPasswordSignUpPOST: emailPasswordImplementation.signUpPOST?.bind(DerivedEP(this)), + generatePasswordResetTokenPOST: emailPasswordImplementation.generatePasswordResetTokenPOST?.bind( + DerivedEP(this), + ), + passwordResetPOST: emailPasswordImplementation.passwordResetPOST?.bind(DerivedEP(this)), + thirdPartySignInUpPOST: thirdPartyImplementation.signInUpPOST?.bind(DerivedTP(this)), + appleRedirectHandlerPOST: thirdPartyImplementation.appleRedirectHandlerPOST?.bind(DerivedTP(this)), + } +} diff --git a/src/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts b/src/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts new file mode 100644 index 000000000..de7bb0b9a --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/api/thirdPartyAPIImplementation.ts @@ -0,0 +1,31 @@ +import { APIInterface } from '../../thirdparty' +import { APIInterface as ThirdPartyEmailPasswordAPIInterface } from '../' + +export default function getIterfaceImpl(apiImplmentation: ThirdPartyEmailPasswordAPIInterface): APIInterface { + const signInUpPOSTFromThirdPartyEmailPassword = apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation) + return { + authorisationUrlGET: apiImplmentation.authorisationUrlGET?.bind(apiImplmentation), + appleRedirectHandlerPOST: apiImplmentation.appleRedirectHandlerPOST?.bind(apiImplmentation), + signInUpPOST: + signInUpPOSTFromThirdPartyEmailPassword === undefined + ? undefined + : async function (input) { + const result = await signInUpPOSTFromThirdPartyEmailPassword(input) + if (result.status === 'OK') { + if (result.user.thirdParty === undefined) + throw new Error('Should never come here') + + return { + ...result, + user: { + ...result.user, + thirdParty: { + ...result.user.thirdParty, + }, + }, + } + } + return result + }, + } +} diff --git a/src/recipe/thirdpartyemailpassword/emaildelivery/index.ts b/src/recipe/thirdpartyemailpassword/emaildelivery/index.ts new file mode 100644 index 000000000..ef147a362 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/emaildelivery/index.ts @@ -0,0 +1 @@ +export * from './services' diff --git a/src/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts b/src/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts new file mode 100644 index 000000000..da2c30a6d --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -0,0 +1,48 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeThirdPartyEmailPasswordEmailDeliveryInput, User } from '../../../types' +import { RecipeInterface as EmailPasswordRecipeInterface } from '../../../../emailpassword' +import { NormalisedAppinfo } from '../../../../../types' +import EmailPasswordBackwardCompatibilityService from '../../../../emailpassword/emaildelivery/services/backwardCompatibility' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' + +export default class BackwardCompatibilityService +implements EmailDeliveryInterface { + private emailPasswordBackwardCompatibilityService: EmailPasswordBackwardCompatibilityService + + constructor( + emailPasswordRecipeInterfaceImpl: EmailPasswordRecipeInterface, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + resetPasswordUsingTokenFeature?: { + createAndSendCustomEmail?: ( + user: User, + passwordResetURLWithToken: string, + userContext: any + ) => Promise + }, + ) { + this.emailPasswordBackwardCompatibilityService = new EmailPasswordBackwardCompatibilityService( + emailPasswordRecipeInterfaceImpl, + appInfo, + isInServerlessEnv, + resetPasswordUsingTokenFeature, + ) + } + + sendEmail = async (input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: any }) => { + await this.emailPasswordBackwardCompatibilityService.sendEmail(input) + } +} diff --git a/src/recipe/thirdpartyemailpassword/emaildelivery/services/index.ts b/src/recipe/thirdpartyemailpassword/emaildelivery/services/index.ts new file mode 100644 index 000000000..cdd3e3292 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/emaildelivery/services/index.ts @@ -0,0 +1,16 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import SMTP from './smtp' +export const SMTPService = SMTP diff --git a/src/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts b/src/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts new file mode 100644 index 000000000..acc2283e3 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts @@ -0,0 +1,30 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeInput } from '../../../../../ingredients/emaildelivery/services/smtp' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' +import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from '../../../types' +import EmailPasswordSMTPService from '../../../../emailpassword/emaildelivery/services/smtp' + +export default class SMTPService implements EmailDeliveryInterface { + private emailPasswordSMTPService: EmailPasswordSMTPService + + constructor(config: TypeInput) { + this.emailPasswordSMTPService = new EmailPasswordSMTPService(config) + } + + sendEmail = async (input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: any }) => { + await this.emailPasswordSMTPService.sendEmail(input) + } +} diff --git a/src/recipe/thirdpartyemailpassword/error.ts b/src/recipe/thirdpartyemailpassword/error.ts new file mode 100644 index 000000000..adac59f4a --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/error.ts @@ -0,0 +1,25 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../../error' + +export default class ThirdPartyEmailPasswordError extends STError { + constructor(options: { type: 'BAD_INPUT_ERROR'; message: string }) { + super({ + ...options, + }) + this.fromRecipe = 'thirdpartyemailpassword' + } +} diff --git a/src/recipe/thirdpartyemailpassword/index.ts b/src/recipe/thirdpartyemailpassword/index.ts new file mode 100644 index 000000000..be213868a --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/index.ts @@ -0,0 +1,160 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import * as thirdPartyProviders from '../thirdparty/providers' +import { TypeProvider } from '../thirdparty/types' +import { TypeEmailPasswordEmailDeliveryInput } from '../emailpassword/types' +import Recipe from './recipe' +import SuperTokensError from './error' +import { APIInterface, EmailPasswordAPIOptions, RecipeInterface, ThirdPartyAPIOptions, User } from './types' + +export default class Wrapper { + static init = Recipe.init + + static Error = SuperTokensError + + static thirdPartySignInUp(thirdPartyId: string, thirdPartyUserId: string, email: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartySignInUp({ + thirdPartyId, + thirdPartyUserId, + email, + userContext, + }) + } + + static getUserByThirdPartyInfo(thirdPartyId: string, thirdPartyUserId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ + thirdPartyId, + thirdPartyUserId, + userContext, + }) + } + + static emailPasswordSignUp(email: string, password: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignUp({ + email, + password, + userContext, + }) + } + + static emailPasswordSignIn(email: string, password: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignIn({ + email, + password, + userContext, + }) + } + + static getUserById(userId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }) + } + + static getUsersByEmail(email: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }) + } + + static createResetPasswordToken(userId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, userContext }) + } + + static resetPasswordUsingToken(token: string, newPassword: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + token, + newPassword, + userContext, + }) + } + + static updateEmailOrPassword(input: { userId: string; email?: string; password?: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ + userContext: {}, + ...input, + }) + } + + static Google = thirdPartyProviders.Google + + static Github = thirdPartyProviders.Github + + static Facebook = thirdPartyProviders.Facebook + + static Apple = thirdPartyProviders.Apple + + static Discord = thirdPartyProviders.Discord + + static GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces + + static Bitbucket = thirdPartyProviders.Bitbucket + + static GitLab = thirdPartyProviders.GitLab + + // static Okta = thirdPartyProviders.Okta; + + // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; + + static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any }) { + return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ + userContext: {}, + ...input, + }) + } +} + +export const init = Wrapper.init + +export const Error = Wrapper.Error + +export const emailPasswordSignUp = Wrapper.emailPasswordSignUp + +export const emailPasswordSignIn = Wrapper.emailPasswordSignIn + +export const thirdPartySignInUp = Wrapper.thirdPartySignInUp + +export const getUserById = Wrapper.getUserById + +export const getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo + +export const getUsersByEmail = Wrapper.getUsersByEmail + +export const createResetPasswordToken = Wrapper.createResetPasswordToken + +export const resetPasswordUsingToken = Wrapper.resetPasswordUsingToken + +export const updateEmailOrPassword = Wrapper.updateEmailOrPassword + +export const Google = Wrapper.Google + +export const Github = Wrapper.Github + +export const Facebook = Wrapper.Facebook + +export const Apple = Wrapper.Apple + +export const Discord = Wrapper.Discord + +export const GoogleWorkspaces = Wrapper.GoogleWorkspaces + +export const Bitbucket = Wrapper.Bitbucket + +export const GitLab = Wrapper.GitLab + +// export let Okta = Wrapper.Okta; + +// export let ActiveDirectory = Wrapper.ActiveDirectory; + +export type { RecipeInterface, TypeProvider, User, APIInterface, EmailPasswordAPIOptions, ThirdPartyAPIOptions } + +export const sendEmail = Wrapper.sendEmail diff --git a/src/recipe/thirdpartyemailpassword/recipe.ts b/src/recipe/thirdpartyemailpassword/recipe.ts new file mode 100644 index 000000000..3bed1b744 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/recipe.ts @@ -0,0 +1,259 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import EmailPasswordRecipe from '../emailpassword/recipe' +import ThirdPartyRecipe from '../thirdparty/recipe' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import STErrorEmailPassword from '../emailpassword/error' +import STErrorThirdParty from '../thirdparty/error' +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import STError from './error' +import { + APIInterface, + RecipeInterface, + TypeInput, + TypeNormalisedInput, + TypeThirdPartyEmailPasswordEmailDeliveryInput, +} from './types' +import { validateAndNormaliseUserInput } from './utils' +import RecipeImplementation from './recipeImplementation' +import EmailPasswordRecipeImplementation from './recipeImplementation/emailPasswordRecipeImplementation' +import ThirdPartyRecipeImplementation from './recipeImplementation/thirdPartyRecipeImplementation' +import getThirdPartyIterfaceImpl from './api/thirdPartyAPIImplementation' +import getEmailPasswordIterfaceImpl from './api/emailPasswordAPIImplementation' +import APIImplementation from './api/implementation' + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined + static RECIPE_ID = 'thirdpartyemailpassword' + + config: TypeNormalisedInput + + private emailPasswordRecipe: EmailPasswordRecipe + + private thirdPartyRecipe: ThirdPartyRecipe | undefined + + recipeInterfaceImpl: RecipeInterface + + apiImpl: APIInterface + + isInServerlessEnv: boolean + + emailDelivery: EmailDeliveryIngredient + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + recipes: { + thirdPartyInstance: ThirdPartyRecipe | undefined + emailPasswordInstance: EmailPasswordRecipe | undefined + }, + ingredients: { + emailDelivery: EmailDeliveryIngredient | undefined + }, + ) { + super(recipeId, appInfo) + this.config = validateAndNormaliseUserInput(this, appInfo, config) + this.isInServerlessEnv = isInServerlessEnv + { + const builder = new OverrideableBuilder( + RecipeImplementation( + Querier.getNewInstanceOrThrowError(EmailPasswordRecipe.RECIPE_ID), + Querier.getNewInstanceOrThrowError(ThirdPartyRecipe.RECIPE_ID), + ), + ) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + + const emailPasswordRecipeImplementation = EmailPasswordRecipeImplementation(this.recipeInterfaceImpl) + /** + * emailDelivery will always needs to be declared after isInServerlessEnv + * and recipeInterfaceImpl values are set + */ + this.emailDelivery + = ingredients.emailDelivery === undefined + ? new EmailDeliveryIngredient( + this.config.getEmailDeliveryConfig(emailPasswordRecipeImplementation, this.isInServerlessEnv), + ) + : ingredients.emailDelivery + + this.emailPasswordRecipe + = recipes.emailPasswordInstance !== undefined + ? recipes.emailPasswordInstance + : new EmailPasswordRecipe( + recipeId, + appInfo, + isInServerlessEnv, + { + override: { + functions: (_) => { + return emailPasswordRecipeImplementation + }, + apis: (_) => { + return getEmailPasswordIterfaceImpl(this.apiImpl) + }, + }, + signUpFeature: { + formFields: this.config.signUpFeature.formFields, + }, + resetPasswordUsingTokenFeature: this.config.resetPasswordUsingTokenFeature, + }, + { + emailDelivery: this.emailDelivery, + }, + ) + + if (this.config.providers.length !== 0) { + this.thirdPartyRecipe + = recipes.thirdPartyInstance !== undefined + ? recipes.thirdPartyInstance + : new ThirdPartyRecipe( + recipeId, + appInfo, + isInServerlessEnv, + { + override: { + functions: (_) => { + return ThirdPartyRecipeImplementation(this.recipeInterfaceImpl) + }, + apis: (_) => { + return getThirdPartyIterfaceImpl(this.apiImpl) + }, + }, + signInAndUpFeature: { + providers: this.config.providers, + }, + }, + {}, + { + emailDelivery: this.emailDelivery, + }, + ) + } + } + + static init(config: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe( + Recipe.RECIPE_ID, + appInfo, + isInServerlessEnv, + config, + { + emailPasswordInstance: undefined, + thirdPartyInstance: undefined, + }, + { + emailDelivery: undefined, + }, + ) + return Recipe.instance + } + else { + throw new Error( + 'ThirdPartyEmailPassword recipe has already been initialised. Please check your code for bugs.', + ) + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + getAPIsHandled = (): APIHandled[] => { + const apisHandled = [...this.emailPasswordRecipe.getAPIsHandled()] + if (this.thirdPartyRecipe !== undefined) + apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()) + + return apisHandled + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + path: NormalisedURLPath, + method: HTTPMethod, + ): Promise => { + if (this.emailPasswordRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined) + return await this.emailPasswordRecipe.handleAPIRequest(id, req, res, path, method) + + if ( + this.thirdPartyRecipe !== undefined + && this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined + ) + return await this.thirdPartyRecipe.handleAPIRequest(id, req, res, path, method) + + return false + } + + handleError = async ( + err: STErrorEmailPassword | STErrorThirdParty, + request: BaseRequest, + response: BaseResponse, + ): Promise => { + if (err.fromRecipe === Recipe.RECIPE_ID) { + throw err + } + else { + if (this.emailPasswordRecipe.isErrorFromThisRecipe(err)) + return await this.emailPasswordRecipe.handleError(err, request, response) + else if (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err)) + return await this.thirdPartyRecipe.handleError(err, request, response) + + throw err + } + } + + getAllCORSHeaders = (): string[] => { + const corsHeaders = [...this.emailPasswordRecipe.getAllCORSHeaders()] + if (this.thirdPartyRecipe !== undefined) + corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()) + + return corsHeaders + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return ( + STError.isErrorFromSuperTokens(err) + && (err.fromRecipe === Recipe.RECIPE_ID + || this.emailPasswordRecipe.isErrorFromThisRecipe(err) + || (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err))) + ) + } +} diff --git a/src/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts b/src/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts new file mode 100644 index 000000000..ab3894142 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts @@ -0,0 +1,60 @@ +import { RecipeInterface, User } from '../../emailpassword/types' +import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from '../types' + +export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { + return { + async signUp(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'EMAIL_ALREADY_EXISTS_ERROR' }> { + return await recipeInterface.emailPasswordSignUp(input) + }, + + async signIn(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'WRONG_CREDENTIALS_ERROR' }> { + return recipeInterface.emailPasswordSignIn(input) + }, + + async getUserById(input: { userId: string; userContext: any }): Promise { + const user = await recipeInterface.getUserById(input) + if (user === undefined || user.thirdParty !== undefined) { + // either user is undefined or it's a thirdparty user. + return undefined + } + return user + }, + + async getUserByEmail(input: { email: string; userContext: any }): Promise { + const result = await recipeInterface.getUsersByEmail(input) + for (let i = 0; i < result.length; i++) { + if (result[i].thirdParty === undefined) + return result[i] + } + return undefined + }, + + async createResetPasswordToken(input: { + userId: string + userContext: any + }): Promise<{ status: 'OK'; token: string } | { status: 'UNKNOWN_USER_ID_ERROR' }> { + return recipeInterface.createResetPasswordToken(input) + }, + + async resetPasswordUsingToken(input: { token: string; newPassword: string; userContext: any }) { + return recipeInterface.resetPasswordUsingToken(input) + }, + + async updateEmailOrPassword(input: { + userId: string + email?: string + password?: string + userContext: any + }): Promise<{ status: 'OK' | 'UNKNOWN_USER_ID_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' }> { + return recipeInterface.updateEmailOrPassword(input) + }, + } +} diff --git a/src/recipe/thirdpartyemailpassword/recipeImplementation/index.ts b/src/recipe/thirdpartyemailpassword/recipeImplementation/index.ts new file mode 100644 index 000000000..49f8a6347 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/recipeImplementation/index.ts @@ -0,0 +1,122 @@ +import { RecipeInterface, User } from '../types' +import EmailPasswordImplemenation from '../../emailpassword/recipeImplementation' + +import ThirdPartyImplemenation from '../../thirdparty/recipeImplementation' +import { RecipeInterface as ThirdPartyRecipeInterface } from '../../thirdparty' +import { Querier } from '../../../querier' +import DerivedEP from './emailPasswordRecipeImplementation' +import DerivedTP from './thirdPartyRecipeImplementation' + +export default function getRecipeInterface( + emailPasswordQuerier: Querier, + thirdPartyQuerier?: Querier, +): RecipeInterface { + const originalEmailPasswordImplementation = EmailPasswordImplemenation(emailPasswordQuerier) + let originalThirdPartyImplementation: undefined | ThirdPartyRecipeInterface + if (thirdPartyQuerier !== undefined) + originalThirdPartyImplementation = ThirdPartyImplemenation(thirdPartyQuerier) + + return { + async emailPasswordSignUp(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'EMAIL_ALREADY_EXISTS_ERROR' }> { + return await originalEmailPasswordImplementation.signUp.bind(DerivedEP(this))(input) + }, + + async emailPasswordSignIn(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'WRONG_CREDENTIALS_ERROR' }> { + return originalEmailPasswordImplementation.signIn.bind(DerivedEP(this))(input) + }, + + async thirdPartySignInUp(input: { + thirdPartyId: string + thirdPartyUserId: string + email: string + userContext: any + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> { + if (originalThirdPartyImplementation === undefined) + throw new Error('No thirdparty provider configured') + + return originalThirdPartyImplementation.signInUp.bind(DerivedTP(this))(input) + }, + + async getUserById(input: { userId: string; userContext: any }): Promise { + const user: User | undefined = await originalEmailPasswordImplementation.getUserById.bind(DerivedEP(this))( + input, + ) + if (user !== undefined) + return user + + if (originalThirdPartyImplementation === undefined) + return undefined + + return await originalThirdPartyImplementation.getUserById.bind(DerivedTP(this))(input) + }, + + async getUsersByEmail({ email, userContext }: { email: string; userContext: any }): Promise { + const userFromEmailPass: User | undefined = await originalEmailPasswordImplementation.getUserByEmail.bind( + DerivedEP(this), + )({ email, userContext }) + + if (originalThirdPartyImplementation === undefined) + return userFromEmailPass === undefined ? [] : [userFromEmailPass] + + const usersFromThirdParty: User[] = await originalThirdPartyImplementation.getUsersByEmail.bind( + DerivedTP(this), + )({ email, userContext }) + + if (userFromEmailPass !== undefined) + return [...usersFromThirdParty, userFromEmailPass] + + return usersFromThirdParty + }, + + async getUserByThirdPartyInfo(input: { + thirdPartyId: string + thirdPartyUserId: string + userContext: any + }): Promise { + if (originalThirdPartyImplementation === undefined) + return undefined + + return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind(DerivedTP(this))(input) + }, + + async createResetPasswordToken(input: { + userId: string + userContext: any + }): Promise<{ status: 'OK'; token: string } | { status: 'UNKNOWN_USER_ID_ERROR' }> { + return originalEmailPasswordImplementation.createResetPasswordToken.bind(DerivedEP(this))(input) + }, + + async resetPasswordUsingToken(input: { token: string; newPassword: string; userContext: any }) { + return originalEmailPasswordImplementation.resetPasswordUsingToken.bind(DerivedEP(this))(input) + }, + + async updateEmailOrPassword( + this: RecipeInterface, + input: { + userId: string + email?: string + password?: string + userContext: any + }, + ): Promise<{ status: 'OK' | 'UNKNOWN_USER_ID_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' }> { + const user = await this.getUserById({ userId: input.userId, userContext: input.userContext }) + if (user === undefined) { + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } + else if (user.thirdParty !== undefined) { + throw new Error('Cannot update email or password of a user who signed up using third party login.') + } + return originalEmailPasswordImplementation.updateEmailOrPassword.bind(DerivedEP(this))(input) + }, + } +} diff --git a/src/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts b/src/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts new file mode 100644 index 000000000..80012f372 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts @@ -0,0 +1,68 @@ +import { RecipeInterface, User } from '../../thirdparty/types' +import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from '../types' + +export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { + return { + async getUserByThirdPartyInfo(input: { + thirdPartyId: string + thirdPartyUserId: string + userContext: any + }): Promise { + const user = await recipeInterface.getUserByThirdPartyInfo(input) + if (user === undefined || user.thirdParty === undefined) + return undefined + + return { + email: user.email, + id: user.id, + timeJoined: user.timeJoined, + thirdParty: user.thirdParty, + } + }, + + async signInUp(input: { + thirdPartyId: string + thirdPartyUserId: string + email: string + userContext: any + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> { + const result = await recipeInterface.thirdPartySignInUp(input) + if (result.user.thirdParty === undefined) + throw new Error('Should never come here') + + return { + status: 'OK', + createdNewUser: result.createdNewUser, + user: { + email: result.user.email, + id: result.user.id, + timeJoined: result.user.timeJoined, + thirdParty: result.user.thirdParty, + }, + } + }, + + async getUserById(input: { userId: string; userContext: any }): Promise { + const user = await recipeInterface.getUserById(input) + if (user === undefined || user.thirdParty === undefined) { + // either user is undefined or it's an email password user. + return undefined + } + return { + email: user.email, + id: user.id, + timeJoined: user.timeJoined, + thirdParty: user.thirdParty, + } + }, + + async getUsersByEmail(input: { email: string; userContext: any }): Promise { + const users = await recipeInterface.getUsersByEmail(input) + + // we filter out all non thirdparty users. + return users.filter((u) => { + return u.thirdParty !== undefined + }) as User[] + }, + } +} diff --git a/src/recipe/thirdpartyemailpassword/types.ts b/src/recipe/thirdpartyemailpassword/types.ts new file mode 100644 index 000000000..5f17eab70 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/types.ts @@ -0,0 +1,297 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import { APIOptions as ThirdPartyAPIOptionsOriginal, TypeProvider } from '../thirdparty/types' +import { + RecipeInterface as EPRecipeInterface, + APIOptions as EmailPasswordAPIOptionsOriginal, + NormalisedFormField, + TypeEmailPasswordEmailDeliveryInput, + TypeFormField, + TypeInputFormField, + TypeInputResetPasswordUsingTokenFeature, +} from '../emailpassword/types' +import { SessionContainerInterface } from '../session/types' +import { + TypeInput as EmailDeliveryTypeInput, + TypeInputWithService as EmailDeliveryTypeInputWithService, +} from '../../ingredients/emaildelivery/types' +import { GeneralErrorResponse } from '../../types' + +export interface User { + id: string + timeJoined: number + email: string + thirdParty?: { + id: string + userId: string + } +} + +export interface TypeContextEmailPasswordSignUp { + loginType: 'emailpassword' + formFields: TypeFormField[] +} + +export interface TypeContextEmailPasswordSignIn { + loginType: 'emailpassword' +} + +export interface TypeContextThirdParty { + loginType: 'thirdparty' + thirdPartyAuthCodeResponse: any +} + +export interface TypeInputSignUp { + formFields?: TypeInputFormField[] +} + +export interface TypeNormalisedInputSignUp { + formFields: NormalisedFormField[] +} + +export interface TypeInput { + signUpFeature?: TypeInputSignUp + providers?: TypeProvider[] + emailDelivery?: EmailDeliveryTypeInput + resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeNormalisedInput { + signUpFeature: TypeNormalisedInputSignUp + providers: TypeProvider[] + getEmailDeliveryConfig: ( + emailPasswordRecipeImpl: EPRecipeInterface, + isInServerlessEnv: boolean + ) => EmailDeliveryTypeInputWithService + resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface RecipeInterface { + getUserById(input: { userId: string; userContext: any }): Promise + + getUsersByEmail(input: { email: string; userContext: any }): Promise + + getUserByThirdPartyInfo(input: { + thirdPartyId: string + thirdPartyUserId: string + userContext: any + }): Promise + + thirdPartySignInUp(input: { + thirdPartyId: string + thirdPartyUserId: string + email: string + userContext: any + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> + + emailPasswordSignUp(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'EMAIL_ALREADY_EXISTS_ERROR' }> + + emailPasswordSignIn(input: { + email: string + password: string + userContext: any + }): Promise<{ status: 'OK'; user: User } | { status: 'WRONG_CREDENTIALS_ERROR' }> + + createResetPasswordToken(input: { + userId: string + userContext: any + }): Promise<{ status: 'OK'; token: string } | { status: 'UNKNOWN_USER_ID_ERROR' }> + + resetPasswordUsingToken(input: { + token: string + newPassword: string + userContext: any + }): Promise< + | { + status: 'OK' + /** + * The id of the user whose password was reset. + * Defined for Core versions 3.9 or later + */ + userId?: string + } + | { status: 'RESET_PASSWORD_INVALID_TOKEN_ERROR' } + > + + updateEmailOrPassword(input: { + userId: string + email?: string + password?: string + userContext: any + }): Promise<{ status: 'OK' | 'UNKNOWN_USER_ID_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' }> + +} + +export type EmailPasswordAPIOptions = EmailPasswordAPIOptionsOriginal + +export type ThirdPartyAPIOptions = ThirdPartyAPIOptionsOriginal +export interface APIInterface { + authorisationUrlGET: + | undefined + | ((input: { + provider: TypeProvider + options: ThirdPartyAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + url: string + } + | GeneralErrorResponse + >) + + emailPasswordEmailExistsGET: + | undefined + | ((input: { + email: string + options: EmailPasswordAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + exists: boolean + } + | GeneralErrorResponse + >) + + generatePasswordResetTokenPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + options: EmailPasswordAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + } + | GeneralErrorResponse + >) + + passwordResetPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + token: string + options: EmailPasswordAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + userId?: string + } + | { + status: 'RESET_PASSWORD_INVALID_TOKEN_ERROR' + } + | GeneralErrorResponse + >) + + thirdPartySignInUpPOST: + | undefined + | ((input: { + provider: TypeProvider + code: string + redirectURI: string + authCodeResponse?: any + clientId?: string + options: ThirdPartyAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + session: SessionContainerInterface + authCodeResponse: any + } + | GeneralErrorResponse + | { + status: 'NO_EMAIL_GIVEN_BY_PROVIDER' + } + >) + + emailPasswordSignInPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + options: EmailPasswordAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + user: User + session: SessionContainerInterface + } + | { + status: 'WRONG_CREDENTIALS_ERROR' + } + | GeneralErrorResponse + >) + + emailPasswordSignUpPOST: + | undefined + | ((input: { + formFields: { + id: string + value: string + }[] + options: EmailPasswordAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + user: User + session: SessionContainerInterface + } + | { + status: 'EMAIL_ALREADY_EXISTS_ERROR' + } + | GeneralErrorResponse + >) + + appleRedirectHandlerPOST: + | undefined + | ((input: { code: string; state: string; options: ThirdPartyAPIOptions; userContext: any }) => Promise) + +} + +export type TypeThirdPartyEmailPasswordEmailDeliveryInput = TypeEmailPasswordEmailDeliveryInput diff --git a/src/recipe/thirdpartyemailpassword/utils.ts b/src/recipe/thirdpartyemailpassword/utils.ts new file mode 100644 index 000000000..a9224eb78 --- /dev/null +++ b/src/recipe/thirdpartyemailpassword/utils.ts @@ -0,0 +1,99 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import { RecipeInterface as EPRecipeInterface, NormalisedFormField } from '../emailpassword/types' +import { normaliseSignUpFormFields } from '../emailpassword/utils' +import { APIInterface, RecipeInterface, TypeInput, TypeInputSignUp, TypeNormalisedInput, TypeNormalisedInputSignUp } from './types' +import Recipe from './recipe' +import BackwardCompatibilityService from './emaildelivery/services/backwardCompatibility' + +export function validateAndNormaliseUserInput( + recipeInstance: Recipe, + appInfo: NormalisedAppinfo, + config?: TypeInput, +): TypeNormalisedInput { + const signUpFeature = validateAndNormaliseSignUpConfig( + recipeInstance, + appInfo, + config === undefined ? undefined : config.signUpFeature, + ) + + const resetPasswordUsingTokenFeature = config === undefined ? undefined : config.resetPasswordUsingTokenFeature + + const providers = ((config === undefined) || (config.providers === undefined)) ? [] : config.providers + + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + function getEmailDeliveryConfig(emailPasswordRecipeImpl: EPRecipeInterface, isInServerlessEnv: boolean) { + let emailService = config?.emailDelivery?.service + /** + * following code is for backward compatibility. + * if user has not passed emailDelivery config, we + * use the createAndSendCustomEmail config. If the user + * has not passed even that config, we use the default + * createAndSendCustomEmail implementation + */ + if (emailService === undefined) { + emailService = new BackwardCompatibilityService( + emailPasswordRecipeImpl, + appInfo, + isInServerlessEnv, + config?.resetPasswordUsingTokenFeature, + ) + } + return { + ...config?.emailDelivery, + /** + * if we do + * let emailDelivery = { + * service: emailService, + * ...config.emailDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + service: emailService, + } + } + + return { + override, + getEmailDeliveryConfig, + signUpFeature, + providers, + resetPasswordUsingTokenFeature, + } +} + +function validateAndNormaliseSignUpConfig( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInputSignUp, +): TypeNormalisedInputSignUp { + const formFields: NormalisedFormField[] = normaliseSignUpFormFields( + config === undefined ? undefined : config.formFields, + ) + + return { + formFields, + } +} diff --git a/src/recipe/thirdpartypasswordless/api/implementation.ts b/src/recipe/thirdpartypasswordless/api/implementation.ts new file mode 100644 index 000000000..88acbe7fa --- /dev/null +++ b/src/recipe/thirdpartypasswordless/api/implementation.ts @@ -0,0 +1,22 @@ +import { APIInterface } from '../types' +import PasswordlessAPIImplementation from '../../passwordless/api/implementation' +import ThirdPartyAPIImplementation from '../../thirdparty/api/implementation' +import DerivedPwdless from './passwordlessAPIImplementation' +import DerivedTP from './thirdPartyAPIImplementation' + +export default function getAPIImplementation(): APIInterface { + const passwordlessImplementation = PasswordlessAPIImplementation() + const thirdPartyImplementation = ThirdPartyAPIImplementation() + return { + consumeCodePOST: passwordlessImplementation.consumeCodePOST?.bind(DerivedPwdless(this)), + createCodePOST: passwordlessImplementation.createCodePOST?.bind(DerivedPwdless(this)), + passwordlessUserEmailExistsGET: passwordlessImplementation.emailExistsGET?.bind(DerivedPwdless(this)), + passwordlessUserPhoneNumberExistsGET: passwordlessImplementation.phoneNumberExistsGET?.bind( + DerivedPwdless(this), + ), + resendCodePOST: passwordlessImplementation.resendCodePOST?.bind(DerivedPwdless(this)), + authorisationUrlGET: thirdPartyImplementation.authorisationUrlGET?.bind(DerivedTP(this)), + thirdPartySignInUpPOST: thirdPartyImplementation.signInUpPOST?.bind(DerivedTP(this)), + appleRedirectHandlerPOST: thirdPartyImplementation.appleRedirectHandlerPOST?.bind(DerivedTP(this)), + } +} diff --git a/src/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.ts b/src/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.ts new file mode 100644 index 000000000..a1df89c30 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/api/passwordlessAPIImplementation.ts @@ -0,0 +1,12 @@ +import { APIInterface } from '../../passwordless' +import { APIInterface as ThirdPartyPasswordlessAPIInterface } from '../types' + +export default function getIterfaceImpl(apiImplmentation: ThirdPartyPasswordlessAPIInterface): APIInterface { + return { + emailExistsGET: apiImplmentation.passwordlessUserEmailExistsGET?.bind(apiImplmentation), + consumeCodePOST: apiImplmentation.consumeCodePOST?.bind(apiImplmentation), + createCodePOST: apiImplmentation.createCodePOST?.bind(apiImplmentation), + phoneNumberExistsGET: apiImplmentation.passwordlessUserPhoneNumberExistsGET?.bind(apiImplmentation), + resendCodePOST: apiImplmentation.resendCodePOST?.bind(apiImplmentation), + } +} diff --git a/src/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts b/src/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts new file mode 100644 index 000000000..85d2830c9 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/api/thirdPartyAPIImplementation.ts @@ -0,0 +1,31 @@ +import { APIInterface } from '../../thirdparty' +import { APIInterface as ThirdPartyPasswordlessAPIInterface } from '../types' + +export default function getIterfaceImpl(apiImplmentation: ThirdPartyPasswordlessAPIInterface): APIInterface { + const signInUpPOSTFromThirdPartyPasswordless = apiImplmentation.thirdPartySignInUpPOST?.bind(apiImplmentation) + return { + authorisationUrlGET: apiImplmentation.authorisationUrlGET?.bind(apiImplmentation), + appleRedirectHandlerPOST: apiImplmentation.appleRedirectHandlerPOST?.bind(apiImplmentation), + signInUpPOST: + signInUpPOSTFromThirdPartyPasswordless === undefined + ? undefined + : async function (input) { + const result = await signInUpPOSTFromThirdPartyPasswordless(input) + if (result.status === 'OK') { + if (!('thirdParty' in result.user)) + throw new Error('Should never come here') + + return { + ...result, + user: { + ...result.user, + thirdParty: { + ...result.user.thirdParty, + }, + }, + } + } + return result + }, + } +} diff --git a/src/recipe/thirdpartypasswordless/emaildelivery/index.ts b/src/recipe/thirdpartypasswordless/emaildelivery/index.ts new file mode 100644 index 000000000..ef147a362 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/emaildelivery/index.ts @@ -0,0 +1 @@ +export * from './services' diff --git a/src/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts b/src/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts new file mode 100644 index 000000000..bf2c8a3f4 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts @@ -0,0 +1,52 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeThirdPartyPasswordlessEmailDeliveryInput } from '../../../types' +import { NormalisedAppinfo } from '../../../../../types' +import PasswordlessBackwardCompatibilityService from '../../../../passwordless/emaildelivery/services/backwardCompatibility' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' + +export default class BackwardCompatibilityService +implements EmailDeliveryInterface { + private passwordlessBackwardCompatibilityService: PasswordlessBackwardCompatibilityService + + constructor( + appInfo: NormalisedAppinfo, + passwordlessFeature?: { + createAndSendCustomEmail?: ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + }, + ) { + this.passwordlessBackwardCompatibilityService = new PasswordlessBackwardCompatibilityService( + appInfo, + passwordlessFeature?.createAndSendCustomEmail, + ) + } + + sendEmail = async (input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any }) => { + await this.passwordlessBackwardCompatibilityService.sendEmail(input) + } +} diff --git a/src/recipe/thirdpartypasswordless/emaildelivery/services/index.ts b/src/recipe/thirdpartypasswordless/emaildelivery/services/index.ts new file mode 100644 index 000000000..cdd3e3292 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/emaildelivery/services/index.ts @@ -0,0 +1,16 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import SMTP from './smtp' +export const SMTPService = SMTP diff --git a/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts b/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts new file mode 100644 index 000000000..b9efe7f6f --- /dev/null +++ b/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts @@ -0,0 +1,55 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { createTransport } from 'nodemailer' +import OverrideableBuilder from 'overrideableBuilder' +import { ServiceInterface, TypeInput } from '../../../../../ingredients/emaildelivery/services/smtp' +import { TypeThirdPartyPasswordlessEmailDeliveryInput } from '../../../types' +import { EmailDeliveryInterface } from '../../../../../ingredients/emaildelivery/types' +import PasswordlessSMTPService from '../../../../passwordless/emaildelivery/services/smtp' +import { getServiceImplementation } from './serviceImplementation' +import getPasswordlessServiceImplementation from './serviceImplementation/passwordlessServiceImplementation' + +export default class SMTPService implements EmailDeliveryInterface { + serviceImpl: ServiceInterface + private passwordlessSMTPService: PasswordlessSMTPService + + constructor(config: TypeInput) { + const transporter = createTransport({ + host: config.smtpSettings.host, + port: config.smtpSettings.port, + auth: { + user: config.smtpSettings.authUsername || config.smtpSettings.from.email, + pass: config.smtpSettings.password, + }, + secure: config.smtpSettings.secure, + }) + let builder = new OverrideableBuilder(getServiceImplementation(transporter, config.smtpSettings.from)) + if (config.override !== undefined) + builder = builder.override(config.override) + + this.serviceImpl = builder.build() + + this.passwordlessSMTPService = new PasswordlessSMTPService({ + smtpSettings: config.smtpSettings, + override: (_) => { + return getPasswordlessServiceImplementation(this.serviceImpl) + }, + }) + } + + sendEmail = async (input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any }) => { + return await this.passwordlessSMTPService.sendEmail(input) + } +} diff --git a/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts b/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts new file mode 100644 index 000000000..456d5fb2f --- /dev/null +++ b/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { Transporter } from 'nodemailer' +import { TypeThirdPartyPasswordlessEmailDeliveryInput } from '../../../../types' +import { + GetContentResult, + ServiceInterface, + TypeInputSendRawEmail, +} from '../../../../../../ingredients/emaildelivery/services/smtp' +import { getServiceImplementation as getPasswordlessServiceImplementation } from '../../../../../passwordless/emaildelivery/services/smtp/serviceImplementation' +import DerivedPwdless from './passwordlessServiceImplementation' + +export function getServiceImplementation( + transporter: Transporter, + from: { + name: string + email: string + }, +): ServiceInterface { + const passwordlessServiceImpl = getPasswordlessServiceImplementation(transporter, from) + return { + async sendRawEmail(input: TypeInputSendRawEmail) { + await transporter.sendMail({ + from: `${from.name} <${from.email}>`, + to: input.toEmail, + subject: input.subject, + html: input.body, + }) + }, + async getContent( + input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any }, + ): Promise { + return await passwordlessServiceImpl.getContent.bind(DerivedPwdless(this))(input) + }, + } +} diff --git a/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts b/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts new file mode 100644 index 000000000..b26948e09 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts @@ -0,0 +1,37 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { TypeThirdPartyPasswordlessEmailDeliveryInput } from '../../../../types' +import { + GetContentResult, + ServiceInterface, + TypeInputSendRawEmail, +} from '../../../../../../ingredients/emaildelivery/services/smtp' +import { TypePasswordlessEmailDeliveryInput } from '../../../../../passwordless/types' + +export default function getServiceInterface( + thirdpartyPasswordlessServiceImplementation: ServiceInterface, +): ServiceInterface { + return { + async sendRawEmail(input: TypeInputSendRawEmail) { + return thirdpartyPasswordlessServiceImplementation.sendRawEmail(input) + }, + async getContent( + input: TypePasswordlessEmailDeliveryInput & { userContext: any }, + ): Promise { + return await thirdpartyPasswordlessServiceImplementation.getContent(input) + }, + } +} diff --git a/src/recipe/thirdpartypasswordless/error.ts b/src/recipe/thirdpartypasswordless/error.ts new file mode 100644 index 000000000..ce32a042a --- /dev/null +++ b/src/recipe/thirdpartypasswordless/error.ts @@ -0,0 +1,25 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from '../../error' + +export default class ThirdPartyEmailPasswordError extends STError { + constructor(options: { type: 'BAD_INPUT_ERROR'; message: string }) { + super({ + ...options, + }) + this.fromRecipe = 'thirdpartypasswordless' + } +} diff --git a/src/recipe/thirdpartypasswordless/index.ts b/src/recipe/thirdpartypasswordless/index.ts new file mode 100644 index 000000000..e8f91352b --- /dev/null +++ b/src/recipe/thirdpartypasswordless/index.ts @@ -0,0 +1,281 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import * as thirdPartyProviders from '../thirdparty/providers' +import { TypeProvider } from '../thirdparty/types' +import { TypePasswordlessSmsDeliveryInput } from '../passwordless/types' +import Recipe from './recipe' +import SuperTokensError from './error' +import { + APIInterface, + PasswordlessAPIOptions, + RecipeInterface, + ThirdPartyAPIOptions, + TypeThirdPartyPasswordlessEmailDeliveryInput, + User, +} from './types' + +export default class Wrapper { + static init = Recipe.init + + static Error = SuperTokensError + + static thirdPartySignInUp(thirdPartyId: string, thirdPartyUserId: string, email: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartySignInUp({ + thirdPartyId, + thirdPartyUserId, + email, + userContext, + }) + } + + static getUserByThirdPartyInfo(thirdPartyId: string, thirdPartyUserId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByThirdPartyInfo({ + thirdPartyId, + thirdPartyUserId, + userContext, + }) + } + + static getUserById(userId: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }) + } + + static getUsersByEmail(email: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }) + } + + static createCode( + input: ( + | { + email: string + } + | { + phoneNumber: string + } + ) & { userInputCode?: string; userContext?: any }, + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ + userContext: {}, + ...input, + }) + } + + static createNewCodeForDevice(input: { deviceId: string; userInputCode?: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ + userContext: {}, + ...input, + }) + } + + static consumeCode( + input: + | { + preAuthSessionId: string + userInputCode: string + deviceId: string + userContext?: any + } + | { + preAuthSessionId: string + linkCode: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ userContext: {}, ...input }) + } + + static getUserByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByPhoneNumber({ userContext: {}, ...input }) + } + + static updatePasswordlessUser(input: { + userId: string + email?: string | null + phoneNumber?: string | null + userContext?: any + }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updatePasswordlessUser({ + userContext: {}, + ...input, + }) + } + + static revokeAllCodes( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ userContext: {}, ...input }) + } + + static revokeCode(input: { codeId: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ userContext: {}, ...input }) + } + + static listCodesByEmail(input: { email: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ userContext: {}, ...input }) + } + + static listCodesByPhoneNumber(input: { phoneNumber: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ + userContext: {}, + ...input, + }) + } + + static listCodesByDeviceId(input: { deviceId: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ userContext: {}, ...input }) + } + + static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; userContext?: any }) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ + userContext: {}, + ...input, + }) + } + + static createMagicLink( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().passwordlessRecipe.createMagicLink({ userContext: {}, ...input }) + } + + static passwordlessSignInUp( + input: + | { + email: string + userContext?: any + } + | { + phoneNumber: string + userContext?: any + }, + ) { + return Recipe.getInstanceOrThrowError().passwordlessRecipe.signInUp({ userContext: {}, ...input }) + } + + static Google = thirdPartyProviders.Google + + static Github = thirdPartyProviders.Github + + static Facebook = thirdPartyProviders.Facebook + + static Apple = thirdPartyProviders.Apple + + static Discord = thirdPartyProviders.Discord + + static GoogleWorkspaces = thirdPartyProviders.GoogleWorkspaces + + static Bitbucket = thirdPartyProviders.Bitbucket + + static GitLab = thirdPartyProviders.GitLab + + // static Okta = thirdPartyProviders.Okta; + + // static ActiveDirectory = thirdPartyProviders.ActiveDirectory; + + static async sendEmail(input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext?: any }) { + return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ + userContext: {}, + ...input, + }) + } + + static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { + return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ + userContext: {}, + ...input, + }) + } +} + +export const init = Wrapper.init + +export const Error = Wrapper.Error + +export const thirdPartySignInUp = Wrapper.thirdPartySignInUp + +export const passwordlessSignInUp = Wrapper.passwordlessSignInUp + +export const getUserById = Wrapper.getUserById + +export const getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo + +export const getUsersByEmail = Wrapper.getUsersByEmail + +export const createCode = Wrapper.createCode + +export const consumeCode = Wrapper.consumeCode + +export const getUserByPhoneNumber = Wrapper.getUserByPhoneNumber + +export const listCodesByDeviceId = Wrapper.listCodesByDeviceId + +export const listCodesByEmail = Wrapper.listCodesByEmail + +export const listCodesByPhoneNumber = Wrapper.listCodesByPhoneNumber + +export const listCodesByPreAuthSessionId = Wrapper.listCodesByPreAuthSessionId + +export const createNewCodeForDevice = Wrapper.createNewCodeForDevice + +export const updatePasswordlessUser = Wrapper.updatePasswordlessUser + +export const revokeAllCodes = Wrapper.revokeAllCodes + +export const revokeCode = Wrapper.revokeCode + +export const createMagicLink = Wrapper.createMagicLink + +export const Google = Wrapper.Google + +export const Github = Wrapper.Github + +export const Facebook = Wrapper.Facebook + +export const Apple = Wrapper.Apple + +export const Discord = Wrapper.Discord + +export const GoogleWorkspaces = Wrapper.GoogleWorkspaces + +export const Bitbucket = Wrapper.Bitbucket + +export const GitLab = Wrapper.GitLab + +// export let Okta = Wrapper.Okta; + +// export let ActiveDirectory = Wrapper.ActiveDirectory; + +export type { RecipeInterface, TypeProvider, User, APIInterface, PasswordlessAPIOptions, ThirdPartyAPIOptions } + +export const sendEmail = Wrapper.sendEmail + +export const sendSms = Wrapper.sendSms diff --git a/src/recipe/thirdpartypasswordless/recipe.ts b/src/recipe/thirdpartypasswordless/recipe.ts new file mode 100644 index 000000000..bf2185d37 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/recipe.ts @@ -0,0 +1,264 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' +import PasswordlessRecipe from '../passwordless/recipe' +import ThirdPartyRecipe from '../thirdparty/recipe' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import STErrorPasswordless from '../passwordless/error' +import STErrorThirdParty from '../thirdparty/error' +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import EmailDeliveryIngredient from '../../ingredients/emaildelivery' +import SmsDeliveryIngredient from '../../ingredients/smsdelivery' +import STError from './error' +import { + APIInterface, + RecipeInterface, + TypeInput, + TypeNormalisedInput, + TypeThirdPartyPasswordlessEmailDeliveryInput, + TypeThirdPartyPasswordlessSmsDeliveryInput, +} from './types' +import { validateAndNormaliseUserInput } from './utils' +import RecipeImplementation from './recipeImplementation' +import PasswordlessRecipeImplementation from './recipeImplementation/passwordlessRecipeImplementation' +import ThirdPartyRecipeImplementation from './recipeImplementation/thirdPartyRecipeImplementation' +import getThirdPartyIterfaceImpl from './api/thirdPartyAPIImplementation' +import getPasswordlessInterfaceImpl from './api/passwordlessAPIImplementation' +import APIImplementation from './api/implementation' + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined + static RECIPE_ID = 'thirdpartypasswordless' + + config: TypeNormalisedInput + + passwordlessRecipe: PasswordlessRecipe + + private thirdPartyRecipe: ThirdPartyRecipe | undefined + + recipeInterfaceImpl: RecipeInterface + + apiImpl: APIInterface + + emailDelivery: EmailDeliveryIngredient + + smsDelivery: SmsDeliveryIngredient + + isInServerlessEnv: boolean + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + recipes: { + thirdPartyInstance: ThirdPartyRecipe | undefined + passwordlessInstance: PasswordlessRecipe | undefined + }, + ingredients: { + emailDelivery: EmailDeliveryIngredient | undefined + smsDelivery: SmsDeliveryIngredient | undefined + }, + ) { + super(recipeId, appInfo) + this.isInServerlessEnv = isInServerlessEnv + this.config = validateAndNormaliseUserInput(appInfo, config) + + { + const builder = new OverrideableBuilder( + RecipeImplementation( + Querier.getNewInstanceOrThrowError(PasswordlessRecipe.RECIPE_ID), + Querier.getNewInstanceOrThrowError(ThirdPartyRecipe.RECIPE_ID), + ), + ) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + { + const builder = new OverrideableBuilder(APIImplementation()) + this.apiImpl = builder.override(this.config.override.apis).build() + } + + this.emailDelivery + = ingredients.emailDelivery === undefined + ? new EmailDeliveryIngredient( + this.config.getEmailDeliveryConfig(this.recipeInterfaceImpl, this.isInServerlessEnv), + ) + : ingredients.emailDelivery + + this.smsDelivery + = ingredients.smsDelivery === undefined + ? new SmsDeliveryIngredient(this.config.getSmsDeliveryConfig()) + : ingredients.smsDelivery + + this.passwordlessRecipe + = recipes.passwordlessInstance !== undefined + ? recipes.passwordlessInstance + : new PasswordlessRecipe( + recipeId, + appInfo, + isInServerlessEnv, + { + ...this.config, + override: { + functions: (_) => { + return PasswordlessRecipeImplementation(this.recipeInterfaceImpl) + }, + apis: (_) => { + return getPasswordlessInterfaceImpl(this.apiImpl) + }, + }, + }, + { + emailDelivery: this.emailDelivery, + smsDelivery: this.smsDelivery, + }, + ) + + if (this.config.providers.length !== 0) { + this.thirdPartyRecipe + = recipes.thirdPartyInstance !== undefined + ? recipes.thirdPartyInstance + : new ThirdPartyRecipe( + recipeId, + appInfo, + isInServerlessEnv, + { + override: { + functions: (_) => { + return ThirdPartyRecipeImplementation(this.recipeInterfaceImpl) + }, + apis: (_) => { + return getThirdPartyIterfaceImpl(this.apiImpl) + }, + }, + signInAndUpFeature: { + providers: this.config.providers, + }, + }, + {}, + { + emailDelivery: this.emailDelivery, + }, + ) + } + } + + static init(config: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe( + Recipe.RECIPE_ID, + appInfo, + isInServerlessEnv, + config, + { + passwordlessInstance: undefined, + thirdPartyInstance: undefined, + }, + { + emailDelivery: undefined, + smsDelivery: undefined, + }, + ) + return Recipe.instance + } + else { + throw new Error( + 'ThirdPartyPasswordless recipe has already been initialised. Please check your code for bugs.', + ) + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + getAPIsHandled = (): APIHandled[] => { + const apisHandled = [...this.passwordlessRecipe.getAPIsHandled()] + if (this.thirdPartyRecipe !== undefined) + apisHandled.push(...this.thirdPartyRecipe.getAPIsHandled()) + + return apisHandled + } + + handleAPIRequest = async ( + id: string, + req: BaseRequest, + res: BaseResponse, + path: NormalisedURLPath, + method: HTTPMethod, + ): Promise => { + if (this.passwordlessRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined) + return await this.passwordlessRecipe.handleAPIRequest(id, req, res, path, method) + + if ( + this.thirdPartyRecipe !== undefined + && this.thirdPartyRecipe.returnAPIIdIfCanHandleRequest(path, method) !== undefined + ) + return await this.thirdPartyRecipe.handleAPIRequest(id, req, res, path, method) + + return false + } + + handleError = async ( + err: STErrorPasswordless | STErrorThirdParty, + request: BaseRequest, + response: BaseResponse, + ): Promise => { + if (err.fromRecipe === Recipe.RECIPE_ID) { + throw err + } + else { + if (this.passwordlessRecipe.isErrorFromThisRecipe(err)) + return await this.passwordlessRecipe.handleError(err, request, response) + else if (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err)) + return await this.thirdPartyRecipe.handleError(err, request, response) + + throw err + } + } + + getAllCORSHeaders = (): string[] => { + const corsHeaders = [...this.passwordlessRecipe.getAllCORSHeaders()] + if (this.thirdPartyRecipe !== undefined) + corsHeaders.push(...this.thirdPartyRecipe.getAllCORSHeaders()) + + return corsHeaders + } + + isErrorFromThisRecipe = (err: any): err is STError => { + return ( + STError.isErrorFromSuperTokens(err) + && (err.fromRecipe === Recipe.RECIPE_ID + || this.passwordlessRecipe.isErrorFromThisRecipe(err) + || (this.thirdPartyRecipe !== undefined && this.thirdPartyRecipe.isErrorFromThisRecipe(err))) + ) + } +} diff --git a/src/recipe/thirdpartypasswordless/recipeImplementation/index.ts b/src/recipe/thirdpartypasswordless/recipeImplementation/index.ts new file mode 100644 index 000000000..f6d006fe3 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/recipeImplementation/index.ts @@ -0,0 +1,117 @@ +import { RecipeInterface, User } from '../types' +import PasswordlessImplemenation from '../../passwordless/recipeImplementation' + +import ThirdPartyImplemenation from '../../thirdparty/recipeImplementation' +import { RecipeInterface as ThirdPartyRecipeInterface } from '../../thirdparty' +import { Querier } from '../../../querier' +import DerivedPwdless from './passwordlessRecipeImplementation' +import DerivedTP from './thirdPartyRecipeImplementation' + +export default function getRecipeInterface(passwordlessQuerier: Querier, thirdPartyQuerier?: Querier): RecipeInterface { + const originalPasswordlessImplementation = PasswordlessImplemenation(passwordlessQuerier) + let originalThirdPartyImplementation: undefined | ThirdPartyRecipeInterface + if (thirdPartyQuerier !== undefined) + originalThirdPartyImplementation = ThirdPartyImplemenation(thirdPartyQuerier) + + return { + async consumeCode(input) { + return originalPasswordlessImplementation.consumeCode.bind(DerivedPwdless(this))(input) + }, + async createCode(input) { + return originalPasswordlessImplementation.createCode.bind(DerivedPwdless(this))(input) + }, + async createNewCodeForDevice(input) { + return originalPasswordlessImplementation.createNewCodeForDevice.bind(DerivedPwdless(this))(input) + }, + async getUserByPhoneNumber(input) { + return originalPasswordlessImplementation.getUserByPhoneNumber.bind(DerivedPwdless(this))(input) + }, + async listCodesByDeviceId(input) { + return originalPasswordlessImplementation.listCodesByDeviceId.bind(DerivedPwdless(this))(input) + }, + async listCodesByEmail(input) { + return originalPasswordlessImplementation.listCodesByEmail.bind(DerivedPwdless(this))(input) + }, + async listCodesByPhoneNumber(input) { + return originalPasswordlessImplementation.listCodesByPhoneNumber.bind(DerivedPwdless(this))(input) + }, + async listCodesByPreAuthSessionId(input) { + return originalPasswordlessImplementation.listCodesByPreAuthSessionId.bind(DerivedPwdless(this))(input) + }, + async revokeAllCodes(input) { + return originalPasswordlessImplementation.revokeAllCodes.bind(DerivedPwdless(this))(input) + }, + async revokeCode(input) { + return originalPasswordlessImplementation.revokeCode.bind(DerivedPwdless(this))(input) + }, + + async updatePasswordlessUser(this: RecipeInterface, input) { + const user = await this.getUserById({ userId: input.userId, userContext: input.userContext }) + if (user === undefined) { + return { + status: 'UNKNOWN_USER_ID_ERROR', + } + } + else if ('thirdParty' in user) { + throw new Error( + 'Cannot update passwordless user info for those who signed up using third party login.', + ) + } + return originalPasswordlessImplementation.updateUser.bind(DerivedPwdless(this))(input) + }, + + async thirdPartySignInUp(input: { + thirdPartyId: string + thirdPartyUserId: string + email: string + userContext: any + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> { + if (originalThirdPartyImplementation === undefined) + throw new Error('No thirdparty provider configured') + + return originalThirdPartyImplementation.signInUp.bind(DerivedTP(this))(input) + }, + + async getUserById(input: { userId: string; userContext: any }): Promise { + const user: User | undefined = await originalPasswordlessImplementation.getUserById.bind( + DerivedPwdless(this), + )(input) + if (user !== undefined) + return user + + if (originalThirdPartyImplementation === undefined) + return undefined + + return await originalThirdPartyImplementation.getUserById.bind(DerivedTP(this))(input) + }, + + async getUsersByEmail({ email, userContext }: { email: string; userContext: any }): Promise { + const userFromEmailPass: User | undefined = await originalPasswordlessImplementation.getUserByEmail.bind( + DerivedPwdless(this), + )({ email, userContext }) + + if (originalThirdPartyImplementation === undefined) + return userFromEmailPass === undefined ? [] : [userFromEmailPass] + + const usersFromThirdParty: User[] = await originalThirdPartyImplementation.getUsersByEmail.bind( + DerivedTP(this), + )({ email, userContext }) + + if (userFromEmailPass !== undefined) + return [...usersFromThirdParty, userFromEmailPass] + + return usersFromThirdParty + }, + + async getUserByThirdPartyInfo(input: { + thirdPartyId: string + thirdPartyUserId: string + userContext: any + }): Promise { + if (originalThirdPartyImplementation === undefined) + return undefined + + return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind(DerivedTP(this))(input) + }, + } +} diff --git a/src/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts b/src/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts new file mode 100644 index 000000000..c7d9126d3 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/recipeImplementation/passwordlessRecipeImplementation.ts @@ -0,0 +1,62 @@ +import { RecipeInterface } from '../../passwordless/types' +import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from '../types' + +export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface { + return { + async consumeCode(input) { + return await recipeInterface.consumeCode(input) + }, + async createCode(input) { + return await recipeInterface.createCode(input) + }, + async createNewCodeForDevice(input) { + return await recipeInterface.createNewCodeForDevice(input) + }, + async getUserByEmail(input) { + const users = await recipeInterface.getUsersByEmail(input) + for (let i = 0; i < users.length; i++) { + const u = users[i] + if (!('thirdParty' in u)) + return u + } + return undefined + }, + async getUserById(input) { + const user = await recipeInterface.getUserById(input) + if (user !== undefined && 'thirdParty' in user) { + // this is a thirdparty user. + return undefined + } + return user + }, + async getUserByPhoneNumber(input) { + const user = await recipeInterface.getUserByPhoneNumber(input) + if (user !== undefined && 'thirdParty' in user) { + // this is a thirdparty user. + return undefined + } + return user + }, + async listCodesByDeviceId(input) { + return await recipeInterface.listCodesByDeviceId(input) + }, + async listCodesByEmail(input) { + return await recipeInterface.listCodesByEmail(input) + }, + async listCodesByPhoneNumber(input) { + return await recipeInterface.listCodesByPhoneNumber(input) + }, + async listCodesByPreAuthSessionId(input) { + return await recipeInterface.listCodesByPreAuthSessionId(input) + }, + async revokeAllCodes(input) { + return await recipeInterface.revokeAllCodes(input) + }, + async revokeCode(input) { + return await recipeInterface.revokeCode(input) + }, + async updateUser(input) { + return await recipeInterface.updatePasswordlessUser(input) + }, + } +} diff --git a/src/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts b/src/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts new file mode 100644 index 000000000..45487e1e2 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts @@ -0,0 +1,53 @@ +import { RecipeInterface, User } from '../../thirdparty/types' +import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from '../types' + +export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface { + return { + async getUserByThirdPartyInfo(input: { + thirdPartyId: string + thirdPartyUserId: string + userContext: any + }): Promise { + const user = await recipeInterface.getUserByThirdPartyInfo(input) + if (user === undefined || !('thirdParty' in user)) + return undefined + + return user + }, + + async signInUp(input: { + thirdPartyId: string + thirdPartyUserId: string + email: string + userContext: any + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> { + const result = await recipeInterface.thirdPartySignInUp(input) + if (!('thirdParty' in result.user)) + throw new Error('Should never come here') + + return { + status: 'OK', + createdNewUser: result.createdNewUser, + user: result.user, + } + }, + + async getUserById(input: { userId: string; userContext: any }): Promise { + const user = await recipeInterface.getUserById(input) + if (user === undefined || !('thirdParty' in user)) { + // either user is undefined or it's an email password user. + return undefined + } + return user + }, + + async getUsersByEmail(input: { email: string; userContext: any }): Promise { + const users = await recipeInterface.getUsersByEmail(input) + + // we filter out all non thirdparty users. + return users.filter((u) => { + return 'thirdParty' in u + }) as User[] + }, + } +} diff --git a/src/recipe/thirdpartypasswordless/smsdelivery/index.ts b/src/recipe/thirdpartypasswordless/smsdelivery/index.ts new file mode 100644 index 000000000..ef147a362 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/smsdelivery/index.ts @@ -0,0 +1 @@ +export * from './services' diff --git a/src/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts b/src/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts new file mode 100644 index 000000000..0bd420db6 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts @@ -0,0 +1,52 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeThirdPartyPasswordlessSmsDeliveryInput } from '../../../types' +import { SmsDeliveryInterface } from '../../../../../ingredients/smsdelivery/types' +import { NormalisedAppinfo } from '../../../../../types' +import PasswordlessBackwardCompatibilityService from '../../../../passwordless/smsdelivery/services/backwardCompatibility' + +export default class BackwardCompatibilityService +implements SmsDeliveryInterface { + private passwordlessBackwardCompatibilityService: PasswordlessBackwardCompatibilityService + + constructor( + appInfo: NormalisedAppinfo, + passwordlessFeature?: { + createAndSendCustomTextMessage?: ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + }, + ) { + this.passwordlessBackwardCompatibilityService = new PasswordlessBackwardCompatibilityService( + appInfo, + passwordlessFeature?.createAndSendCustomTextMessage, + ) + } + + sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: any }) => { + await this.passwordlessBackwardCompatibilityService.sendSms(input) + } +} diff --git a/src/recipe/thirdpartypasswordless/smsdelivery/services/index.ts b/src/recipe/thirdpartypasswordless/smsdelivery/services/index.ts new file mode 100644 index 000000000..f7e1ab546 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/smsdelivery/services/index.ts @@ -0,0 +1,19 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import Twilio from './twilio' +import Supertokens from './supertokens' + +export const TwilioService = Twilio +export const SupertokensService = Supertokens diff --git a/src/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts b/src/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts new file mode 100644 index 000000000..fa746ab1c --- /dev/null +++ b/src/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts @@ -0,0 +1,29 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { SmsDeliveryInterface } from '../../../../../ingredients/smsdelivery/types' +import { TypeThirdPartyPasswordlessSmsDeliveryInput } from '../../../types' +import PasswordlessSupertokensService from '../../../../passwordless/smsdelivery/services/supertokens' + +export default class SupertokensService implements SmsDeliveryInterface { + private passwordlessSupertokensService: PasswordlessSupertokensService + + constructor(apiKey: string) { + this.passwordlessSupertokensService = new PasswordlessSupertokensService(apiKey) + } + + sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: any }) => { + await this.passwordlessSupertokensService.sendSms(input) + } +} diff --git a/src/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts b/src/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts new file mode 100644 index 000000000..65eac816d --- /dev/null +++ b/src/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts @@ -0,0 +1,30 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { TypeInput } from '../../../../../ingredients/smsdelivery/services/twilio' +import { SmsDeliveryInterface } from '../../../../../ingredients/smsdelivery/types' +import { TypeThirdPartyPasswordlessSmsDeliveryInput } from '../../../types' +import PasswordlessTwilioService from '../../../../passwordless/smsdelivery/services/twilio/index' + +export default class TwilioService implements SmsDeliveryInterface { + private passwordlessTwilioService: PasswordlessTwilioService + + constructor(config: TypeInput) { + this.passwordlessTwilioService = new PasswordlessTwilioService(config) + } + + sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: any }) => { + await this.passwordlessTwilioService.sendSms(input) + } +} diff --git a/src/recipe/thirdpartypasswordless/types.ts b/src/recipe/thirdpartypasswordless/types.ts new file mode 100644 index 000000000..2cf9402b9 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/types.ts @@ -0,0 +1,465 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import OverrideableBuilder from 'overrideableBuilder' +import { APIOptions as ThirdPartyAPIOptionsOriginal, TypeProvider } from '../thirdparty/types' +import { + DeviceType as DeviceTypeOriginal, + APIOptions as PasswordlessAPIOptionsOriginal, + TypePasswordlessEmailDeliveryInput, + TypePasswordlessSmsDeliveryInput, +} from '../passwordless/types' +import { SessionContainerInterface } from '../session/types' +import { + TypeInput as EmailDeliveryTypeInput, + TypeInputWithService as EmailDeliveryTypeInputWithService, +} from '../../ingredients/emaildelivery/types' +import { + TypeInput as SmsDeliveryTypeInput, + TypeInputWithService as SmsDeliveryTypeInputWithService, +} from '../../ingredients/smsdelivery/types' +import { GeneralErrorResponse } from '../../types' + +export type DeviceType = DeviceTypeOriginal + +export type User = ( + | { + // passwordless user properties + email?: string + phoneNumber?: string + } + | { + // third party user properties + email: string + thirdParty: { + id: string + userId: string + } + } +) & { + id: string + timeJoined: number +} + +export type TypeInput = ( + | { + contactMethod: 'PHONE' + validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use smsDelivery config instead + */ + createAndSendCustomTextMessage?: ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + } + | { + contactMethod: 'EMAIL' + validateEmailAddress?: (email: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use emailDelivery config instead + */ + createAndSendCustomEmail?: ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + } + | { + contactMethod: 'EMAIL_OR_PHONE' + validateEmailAddress?: (email: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use emailDelivery config instead + */ + createAndSendCustomEmail?: ( + input: { + // Where the message should be delivered. + email: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined + + // Override to use custom template/contact method + /** + * @deprecated Please use smsDelivery config instead + */ + createAndSendCustomTextMessage?: ( + input: { + // Where the message should be delivered. + phoneNumber: string + // This has to be entered on the starting device to finish sign in/up + userInputCode?: string + // Full url that the end-user can click to finish sign in/up + urlWithLinkCode?: string + codeLifetime: number + // Unlikely, but someone could display this (or a derived thing) to identify the device + preAuthSessionId: string + }, + userContext: any + ) => Promise + } +) & { + /** + * Unlike passwordless recipe, emailDelivery config is outside here because regardless + * of `contactMethod` value, the config is required for email verification recipe + */ + emailDelivery?: EmailDeliveryTypeInput + smsDelivery?: SmsDeliveryTypeInput + providers?: TypeProvider[] + flowType: 'USER_INPUT_CODE' | 'MAGIC_LINK' | 'USER_INPUT_CODE_AND_MAGIC_LINK' + + // Override this to override how user input codes are generated + // By default (=undefined) it is done in the Core + getCustomUserInputCode?: (userContext: any) => Promise | string + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export type TypeNormalisedInput = ( + | { + contactMethod: 'PHONE' + validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined + } + | { + contactMethod: 'EMAIL' + validateEmailAddress?: (email: string) => Promise | string | undefined + } + | { + contactMethod: 'EMAIL_OR_PHONE' + validateEmailAddress?: (email: string) => Promise | string | undefined + validatePhoneNumber?: (phoneNumber: string) => Promise | string | undefined + } +) & { + flowType: 'USER_INPUT_CODE' | 'MAGIC_LINK' | 'USER_INPUT_CODE_AND_MAGIC_LINK' + + // Override this to override how user input codes are generated + // By default (=undefined) it is done in the Core + getCustomUserInputCode?: (userContext: any) => Promise | string + providers: TypeProvider[] + getEmailDeliveryConfig: ( + recipeImpl: RecipeInterface, + isInServerlessEnv: boolean + ) => EmailDeliveryTypeInputWithService + getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface RecipeInterface { + getUserById(input: { userId: string; userContext: any }): Promise + + getUsersByEmail(input: { email: string; userContext: any }): Promise + + getUserByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise + + getUserByThirdPartyInfo(input: { + thirdPartyId: string + thirdPartyUserId: string + userContext: any + }): Promise + + thirdPartySignInUp(input: { + thirdPartyId: string + thirdPartyUserId: string + email: string + userContext: any + }): Promise<{ status: 'OK'; createdNewUser: boolean; user: User }> + + createCode: ( + input: ( + | { + email: string + } + | { + phoneNumber: string + } + ) & { userInputCode?: string; userContext: any } + ) => Promise<{ + status: 'OK' + preAuthSessionId: string + codeId: string + deviceId: string + userInputCode: string + linkCode: string + codeLifetime: number + timeCreated: number + }> + + createNewCodeForDevice: (input: { + deviceId: string + userInputCode?: string + userContext: any + }) => Promise< + | { + status: 'OK' + preAuthSessionId: string + codeId: string + deviceId: string + userInputCode: string + linkCode: string + codeLifetime: number + timeCreated: number + } + | { status: 'RESTART_FLOW_ERROR' | 'USER_INPUT_CODE_ALREADY_USED_ERROR' } + > + + consumeCode: ( + input: + | { + userInputCode: string + deviceId: string + preAuthSessionId: string + userContext: any + } + | { + linkCode: string + preAuthSessionId: string + userContext: any + } + ) => Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + } + | { + status: 'INCORRECT_USER_INPUT_CODE_ERROR' | 'EXPIRED_USER_INPUT_CODE_ERROR' + failedCodeInputAttemptCount: number + maximumCodeInputAttempts: number + } + | { status: 'RESTART_FLOW_ERROR' } + > + + updatePasswordlessUser: (input: { + userId: string + email?: string | null + phoneNumber?: string | null + userContext: any + }) => Promise<{ + status: 'OK' | 'UNKNOWN_USER_ID_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' | 'PHONE_NUMBER_ALREADY_EXISTS_ERROR' + }> + + revokeAllCodes: ( + input: + | { + email: string + userContext: any + } + | { + phoneNumber: string + userContext: any + } + ) => Promise<{ + status: 'OK' + }> + + revokeCode: (input: { + codeId: string + userContext: any + }) => Promise<{ + status: 'OK' + }> + + listCodesByEmail: (input: { email: string; userContext: any }) => Promise + + listCodesByPhoneNumber: (input: { phoneNumber: string; userContext: any }) => Promise + + listCodesByDeviceId: (input: { deviceId: string; userContext: any }) => Promise + + listCodesByPreAuthSessionId: (input: { + preAuthSessionId: string + userContext: any + }) => Promise +} + +export type PasswordlessAPIOptions = PasswordlessAPIOptionsOriginal + +export type ThirdPartyAPIOptions = ThirdPartyAPIOptionsOriginal +export interface APIInterface { + authorisationUrlGET: + | undefined + | ((input: { + provider: TypeProvider + options: ThirdPartyAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + url: string + } + | GeneralErrorResponse + >) + + thirdPartySignInUpPOST: + | undefined + | ((input: { + provider: TypeProvider + code: string + redirectURI: string + authCodeResponse?: any + clientId?: string + options: ThirdPartyAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + session: SessionContainerInterface + authCodeResponse: any + } + | GeneralErrorResponse + | { + status: 'NO_EMAIL_GIVEN_BY_PROVIDER' + } + >) + + appleRedirectHandlerPOST: + | undefined + | ((input: { code: string; state: string; options: ThirdPartyAPIOptions; userContext: any }) => Promise) + + createCodePOST: + | undefined + | (( + input: ({ email: string } | { phoneNumber: string }) & { + options: PasswordlessAPIOptions + userContext: any + } + ) => Promise< + | { + status: 'OK' + deviceId: string + preAuthSessionId: string + flowType: 'USER_INPUT_CODE' | 'MAGIC_LINK' | 'USER_INPUT_CODE_AND_MAGIC_LINK' + } + | GeneralErrorResponse + >) + + resendCodePOST: + | undefined + | (( + input: { deviceId: string; preAuthSessionId: string } & { + options: PasswordlessAPIOptions + userContext: any + } + ) => Promise) + + consumeCodePOST: + | undefined + | (( + input: ( + | { + userInputCode: string + deviceId: string + preAuthSessionId: string + } + | { + linkCode: string + preAuthSessionId: string + } + ) & { + options: PasswordlessAPIOptions + userContext: any + } + ) => Promise< + | { + status: 'OK' + createdNewUser: boolean + user: User + session: SessionContainerInterface + } + | { + status: 'INCORRECT_USER_INPUT_CODE_ERROR' | 'EXPIRED_USER_INPUT_CODE_ERROR' + failedCodeInputAttemptCount: number + maximumCodeInputAttempts: number + } + | GeneralErrorResponse + | { status: 'RESTART_FLOW_ERROR' } + >) + + passwordlessUserEmailExistsGET: + | undefined + | ((input: { + email: string + options: PasswordlessAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + exists: boolean + } + | GeneralErrorResponse + >) + + passwordlessUserPhoneNumberExistsGET: + | undefined + | ((input: { + phoneNumber: string + options: PasswordlessAPIOptions + userContext: any + }) => Promise< + | { + status: 'OK' + exists: boolean + } + | GeneralErrorResponse + >) +} + +export type TypeThirdPartyPasswordlessEmailDeliveryInput = TypePasswordlessEmailDeliveryInput + +export type TypeThirdPartyPasswordlessSmsDeliveryInput = TypePasswordlessSmsDeliveryInput diff --git a/src/recipe/thirdpartypasswordless/utils.ts b/src/recipe/thirdpartypasswordless/utils.ts new file mode 100644 index 000000000..fc24b6699 --- /dev/null +++ b/src/recipe/thirdpartypasswordless/utils.ts @@ -0,0 +1,101 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import BackwardCompatibilityEmailService from './emaildelivery/services/backwardCompatibility' +import BackwardCompatibilitySmsService from './smsdelivery/services/backwardCompatibility' + +export function validateAndNormaliseUserInput(appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { + const providers = config.providers === undefined ? [] : config.providers + + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + function getEmailDeliveryConfig() { + let emailService = config?.emailDelivery?.service + /** + * following code is for backward compatibility. + * if user has not passed emailDelivery config, we + * use the createAndSendCustomEmail config. If the user + * has not passed even that config, we use the default + * createAndSendCustomEmail implementation + */ + if (emailService === undefined) { + emailService = new BackwardCompatibilityEmailService(appInfo, { + createAndSendCustomEmail: + config?.contactMethod !== 'PHONE' ? config?.createAndSendCustomEmail : undefined, + }) + } + return { + ...config?.emailDelivery, + /** + * if we do + * let emailDelivery = { + * service: emailService, + * ...config.emailDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + service: emailService, + } + } + + function getSmsDeliveryConfig() { + let smsService = config?.smsDelivery?.service + /** + * following code is for backward compatibility. + * if user has not passed smsDelivery config, we + * use the createAndSendCustomTextMessage config. If the user + * has not passed even that config, we use the default + * createAndSendCustomTextMessage implementation + */ + if (smsService === undefined) { + smsService = new BackwardCompatibilitySmsService(appInfo, { + createAndSendCustomTextMessage: + config?.contactMethod !== 'EMAIL' ? config?.createAndSendCustomTextMessage : undefined, + }) + } + return { + ...config?.smsDelivery, + /** + * if we do + * let smsDelivery = { + * service: smsService, + * ...config.smsDelivery, + * }; + * + * and if the user has passed service as undefined, + * it it again get set to undefined, so we + * set service at the end + */ + service: smsService, + } + } + + return { + ...config, + providers, + override, + getEmailDeliveryConfig, + getSmsDeliveryConfig, + } +} diff --git a/src/recipe/usermetadata/index.ts b/src/recipe/usermetadata/index.ts new file mode 100644 index 000000000..ee8434119 --- /dev/null +++ b/src/recipe/usermetadata/index.ts @@ -0,0 +1,51 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { JSONObject } from '../../types' +import Recipe from './recipe' +import { RecipeInterface } from './types' + +export default class Wrapper { + static init = Recipe.init + + static async getUserMetadata(userId: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserMetadata({ + userId, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async updateUserMetadata(userId: string, metadataUpdate: JSONObject, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateUserMetadata({ + userId, + metadataUpdate, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async clearUserMetadata(userId: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.clearUserMetadata({ + userId, + userContext: userContext === undefined ? {} : userContext, + }) + } +} + +export const init = Wrapper.init +export const getUserMetadata = Wrapper.getUserMetadata +export const updateUserMetadata = Wrapper.updateUserMetadata +export const clearUserMetadata = Wrapper.clearUserMetadata + +export type { RecipeInterface, JSONObject } diff --git a/src/recipe/usermetadata/recipe.ts b/src/recipe/usermetadata/recipe.ts new file mode 100644 index 000000000..a4a088b0b --- /dev/null +++ b/src/recipe/usermetadata/recipe.ts @@ -0,0 +1,106 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import SuperTokensError from '../../error' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import normalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' + +import RecipeImplementation from './recipeImplementation' +import { RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import { validateAndNormaliseUserInput } from './utils' + +export default class Recipe extends RecipeModule { + static RECIPE_ID = 'usermetadata' + private static instance: Recipe | undefined = undefined + + config: TypeNormalisedInput + recipeInterfaceImpl: RecipeInterface + isInServerlessEnv: boolean + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo) + this.config = validateAndNormaliseUserInput(this, appInfo, config) + this.isInServerlessEnv = isInServerlessEnv + + { + const builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + } + + /* Init functions */ + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error( + 'Initialisation not done. Did you forget to call the UserMetadata.init or SuperTokens.init function?', + ) + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config) + return Recipe.instance + } + else { + throw new Error('UserMetadata recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + /* RecipeModule functions */ + + getAPIsHandled(): APIHandled[] { + return [] + } + + // This stub is required to implement RecipeModule + handleAPIRequest = async ( + _: string, + __: BaseRequest, + ___: BaseResponse, + ____: normalisedURLPath, + _____: HTTPMethod, + ): Promise => { + throw new Error('Should never come here') + } + + handleError(error: SuperTokensError, _: BaseRequest, __: BaseResponse): Promise { + throw error + } + + getAllCORSHeaders(): string[] { + return [] + } + + isErrorFromThisRecipe(err: any): err is SuperTokensError { + return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } +} diff --git a/src/recipe/usermetadata/recipeImplementation.ts b/src/recipe/usermetadata/recipeImplementation.ts new file mode 100644 index 000000000..a4d568a55 --- /dev/null +++ b/src/recipe/usermetadata/recipeImplementation.ts @@ -0,0 +1,39 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import { RecipeInterface } from '.' + +export default function getRecipeInterface(querier: Querier): RecipeInterface { + return { + getUserMetadata({ userId }) { + return querier.sendGetRequest(new NormalisedURLPath('/recipe/user/metadata'), { userId }) + }, + + updateUserMetadata({ userId, metadataUpdate }) { + return querier.sendPutRequest(new NormalisedURLPath('/recipe/user/metadata'), { + userId, + metadataUpdate, + }) + }, + + clearUserMetadata({ userId }) { + return querier.sendPostRequest(new NormalisedURLPath('/recipe/user/metadata/remove'), { + userId, + }) + }, + } +} diff --git a/src/recipe/usermetadata/types.ts b/src/recipe/usermetadata/types.ts new file mode 100644 index 000000000..f4a285f31 --- /dev/null +++ b/src/recipe/usermetadata/types.ts @@ -0,0 +1,74 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import { JSONObject } from '../../types' + +export interface TypeInput { + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeNormalisedInput { + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface APIInterface {} + +export interface RecipeInterface { + getUserMetadata: (input: { + userId: string + userContext: any + }) => Promise<{ + status: 'OK' + metadata: any + }> + + /** + * Updates the metadata object of the user by doing a shallow merge of the stored and the update JSONs + * and removing properties set to null on the root level of the update object. + * e.g.: + * - stored: `{ "preferences": { "theme":"dark" }, "notifications": { "email": true }, "todos": ["example"] }` + * - update: `{ "notifications": { "sms": true }, "todos": null }` + * - result: `{ "preferences": { "theme":"dark" }, "notifications": { "sms": true } }` + */ + updateUserMetadata: (input: { + userId: string + metadataUpdate: JSONObject + userContext: any + }) => Promise<{ + status: 'OK' + metadata: JSONObject + }> + + clearUserMetadata: (input: { + userId: string + userContext: any + }) => Promise<{ + status: 'OK' + }> + +} diff --git a/src/recipe/usermetadata/utils.ts b/src/recipe/usermetadata/utils.ts new file mode 100644 index 000000000..f04ddd2ca --- /dev/null +++ b/src/recipe/usermetadata/utils.ts @@ -0,0 +1,34 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import Recipe from './recipe' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' + +export function validateAndNormaliseUserInput( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInput, +): TypeNormalisedInput { + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + return { + override, + } +} diff --git a/src/recipe/userroles/index.ts b/src/recipe/userroles/index.ts new file mode 100644 index 000000000..7d4214ee0 --- /dev/null +++ b/src/recipe/userroles/index.ts @@ -0,0 +1,114 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { PermissionClaim } from './permissionClaim' +import Recipe from './recipe' +import { RecipeInterface } from './types' +import { UserRoleClaim } from './userRoleClaim' + +export default class Wrapper { + static init = Recipe.init + static PermissionClaim = PermissionClaim + static UserRoleClaim = UserRoleClaim + + static async addRoleToUser(userId: string, role: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.addRoleToUser({ + userId, + role, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async removeUserRole(userId: string, role: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removeUserRole({ + userId, + role, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async getRolesForUser(userId: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getRolesForUser({ + userId, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async getUsersThatHaveRole(role: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersThatHaveRole({ + role, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewRoleOrAddPermissions({ + role, + permissions, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async getPermissionsForRole(role: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async removePermissionsFromRole(role: string, permissions: string[], userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removePermissionsFromRole({ + role, + permissions, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async getRolesThatHavePermission(permission: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getRolesThatHavePermission({ + permission, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async deleteRole(role: string, userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.deleteRole({ + role, + userContext: userContext === undefined ? {} : userContext, + }) + } + + static async getAllRoles(userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getAllRoles({ + userContext: userContext === undefined ? {} : userContext, + }) + } +} + +export const init = Wrapper.init +export const addRoleToUser = Wrapper.addRoleToUser +export const removeUserRole = Wrapper.removeUserRole +export const getRolesForUser = Wrapper.getRolesForUser +export const getUsersThatHaveRole = Wrapper.getUsersThatHaveRole +export const createNewRoleOrAddPermissions = Wrapper.createNewRoleOrAddPermissions +export const getPermissionsForRole = Wrapper.getPermissionsForRole +export const removePermissionsFromRole = Wrapper.removePermissionsFromRole +export const getRolesThatHavePermission = Wrapper.getRolesThatHavePermission +export const deleteRole = Wrapper.deleteRole +export const getAllRoles = Wrapper.getAllRoles +export { UserRoleClaim } from './userRoleClaim' +export { PermissionClaim } from './permissionClaim' + +export type { RecipeInterface } diff --git a/src/recipe/userroles/permissionClaim.ts b/src/recipe/userroles/permissionClaim.ts new file mode 100644 index 000000000..572537f8b --- /dev/null +++ b/src/recipe/userroles/permissionClaim.ts @@ -0,0 +1,40 @@ +import { PrimitiveArrayClaim } from '../session/claimBaseClasses/primitiveArrayClaim' +import UserRoleRecipe from './recipe' + +/** + * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. + * */ +export class PermissionClaimClass extends PrimitiveArrayClaim { + constructor() { + super({ + key: 'st-perm', + async fetchValue(userId, userContext) { + const recipe = UserRoleRecipe.getInstanceOrThrowError() + + // We fetch the roles because the rolesClaim may not be present in the payload + const userRoles = await recipe.recipeInterfaceImpl.getRolesForUser({ + userId, + userContext, + }) + + // We use a set to filter out duplicates + const userPermissions = new Set() + for (const role of userRoles.roles) { + const rolePermissions = await recipe.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }) + if (rolePermissions.status === 'OK') { + for (const perm of rolePermissions.permissions) + userPermissions.add(perm) + } + } + + return Array.from(userPermissions) + }, + defaultMaxAgeInSeconds: 300, + }) + } +} + +export const PermissionClaim = new PermissionClaimClass() diff --git a/src/recipe/userroles/recipe.ts b/src/recipe/userroles/recipe.ts new file mode 100644 index 000000000..af4170be1 --- /dev/null +++ b/src/recipe/userroles/recipe.ts @@ -0,0 +1,118 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' +import SuperTokensError from '../../error' +import { BaseRequest } from '../../framework/request' +import { BaseResponse } from '../../framework/response' +import normalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import RecipeModule from '../../recipeModule' +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from '../../types' + +import { PostSuperTokensInitCallbacks } from '../../postSuperTokensInitCallbacks' +import SessionRecipe from '../session/recipe' +import RecipeImplementation from './recipeImplementation' +import { RecipeInterface, TypeInput, TypeNormalisedInput } from './types' +import { validateAndNormaliseUserInput } from './utils' +import { UserRoleClaim } from './userRoleClaim' +import { PermissionClaim } from './permissionClaim' + +export default class Recipe extends RecipeModule { + static RECIPE_ID = 'userroles' + private static instance: Recipe | undefined = undefined + + config: TypeNormalisedInput + recipeInterfaceImpl: RecipeInterface + isInServerlessEnv: boolean + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo) + this.config = validateAndNormaliseUserInput(this, appInfo, config) + this.isInServerlessEnv = isInServerlessEnv + + { + const builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))) + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build() + } + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + if (!this.config.skipAddingRolesToAccessToken) + SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(UserRoleClaim) + + if (!this.config.skipAddingPermissionsToAccessToken) + SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(PermissionClaim) + }) + } + + /* Init functions */ + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) + return Recipe.instance + + throw new Error( + 'Initialisation not done. Did you forget to call the UserRoles.init or SuperTokens.init functions?', + ) + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config) + return Recipe.instance + } + else { + throw new Error('UserRoles recipe has already been initialised. Please check your code for bugs.') + } + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Recipe.instance = undefined + } + + /* RecipeModule functions */ + + getAPIsHandled(): APIHandled[] { + return [] + } + + // This stub is required to implement RecipeModule + handleAPIRequest = async ( + _: string, + __: BaseRequest, + ___: BaseResponse, + ____: normalisedURLPath, + _____: HTTPMethod, + ): Promise => { + throw new Error('Should never come here') + } + + handleError(error: SuperTokensError, _: BaseRequest, __: BaseResponse): Promise { + throw error + } + + getAllCORSHeaders(): string[] { + return [] + } + + isErrorFromThisRecipe(err: any): err is SuperTokensError { + return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID + } +} diff --git a/src/recipe/userroles/recipeImplementation.ts b/src/recipe/userroles/recipeImplementation.ts new file mode 100644 index 000000000..392a7f467 --- /dev/null +++ b/src/recipe/userroles/recipeImplementation.ts @@ -0,0 +1,65 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import NormalisedURLPath from '../../normalisedURLPath' +import { Querier } from '../../querier' +import { RecipeInterface } from './types' + +export default function getRecipeInterface(querier: Querier): RecipeInterface { + return { + addRoleToUser({ userId, role }) { + return querier.sendPutRequest(new NormalisedURLPath('/recipe/user/role'), { userId, role }) + }, + + removeUserRole({ userId, role }) { + return querier.sendPostRequest(new NormalisedURLPath('/recipe/user/role/remove'), { userId, role }) + }, + + getRolesForUser({ userId }) { + return querier.sendGetRequest(new NormalisedURLPath('/recipe/user/roles'), { userId }) + }, + + getUsersThatHaveRole({ role }) { + return querier.sendGetRequest(new NormalisedURLPath('/recipe/role/users'), { role }) + }, + + createNewRoleOrAddPermissions({ role, permissions }) { + return querier.sendPutRequest(new NormalisedURLPath('/recipe/role'), { role, permissions }) + }, + + getPermissionsForRole({ role }) { + return querier.sendGetRequest(new NormalisedURLPath('/recipe/role/permissions'), { role }) + }, + + removePermissionsFromRole({ role, permissions }) { + return querier.sendPostRequest(new NormalisedURLPath('/recipe/role/permissions/remove'), { + role, + permissions, + }) + }, + + getRolesThatHavePermission({ permission }) { + return querier.sendGetRequest(new NormalisedURLPath('/recipe/permission/roles'), { permission }) + }, + + deleteRole({ role }) { + return querier.sendPostRequest(new NormalisedURLPath('/recipe/role/remove'), { role }) + }, + + getAllRoles() { + return querier.sendGetRequest(new NormalisedURLPath('/recipe/roles'), {}) + }, + } +} diff --git a/src/recipe/userroles/types.ts b/src/recipe/userroles/types.ts new file mode 100644 index 000000000..b8d7697dc --- /dev/null +++ b/src/recipe/userroles/types.ts @@ -0,0 +1,146 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import OverrideableBuilder from 'overrideableBuilder' + +export interface TypeInput { + skipAddingRolesToAccessToken?: boolean + skipAddingPermissionsToAccessToken?: boolean + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface TypeNormalisedInput { + skipAddingRolesToAccessToken: boolean + skipAddingPermissionsToAccessToken: boolean + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface + } +} + +export interface APIInterface {} + +export interface RecipeInterface { + addRoleToUser: (input: { + userId: string + role: string + userContext: any + }) => Promise< + | { + status: 'OK' + didUserAlreadyHaveRole: boolean + } + | { + status: 'UNKNOWN_ROLE_ERROR' + } + > + + removeUserRole: (input: { + userId: string + role: string + userContext: any + }) => Promise< + | { + status: 'OK' + didUserHaveRole: boolean + } + | { + status: 'UNKNOWN_ROLE_ERROR' + } + > + + getRolesForUser: (input: { + userId: string + userContext: any + }) => Promise<{ + status: 'OK' + roles: string[] + }> + + getUsersThatHaveRole: (input: { + role: string + userContext: any + }) => Promise< + | { + status: 'OK' + users: string[] + } + | { + status: 'UNKNOWN_ROLE_ERROR' + } + > + + createNewRoleOrAddPermissions: (input: { + role: string + permissions: string[] + userContext: any + }) => Promise<{ + status: 'OK' + createdNewRole: boolean + }> + + getPermissionsForRole: (input: { + role: string + userContext: any + }) => Promise< + | { + status: 'OK' + permissions: string[] + } + | { + status: 'UNKNOWN_ROLE_ERROR' + } + > + + removePermissionsFromRole: (input: { + role: string + permissions: string[] + userContext: any + }) => Promise<{ + status: 'OK' | 'UNKNOWN_ROLE_ERROR' + }> + + getRolesThatHavePermission: (input: { + permission: string + userContext: any + }) => Promise<{ + status: 'OK' + roles: string[] + }> + + deleteRole: (input: { + role: string + userContext: any + }) => Promise<{ + status: 'OK' + didRoleExist: boolean + }> + + getAllRoles: (input: { + userContext: any + }) => Promise<{ + status: 'OK' + roles: string[] + }> +} diff --git a/src/recipe/userroles/userRoleClaim.ts b/src/recipe/userroles/userRoleClaim.ts new file mode 100644 index 000000000..31badccfb --- /dev/null +++ b/src/recipe/userroles/userRoleClaim.ts @@ -0,0 +1,24 @@ +import { PrimitiveArrayClaim } from '../session/claimBaseClasses/primitiveArrayClaim' +import UserRoleRecipe from './recipe' + +/** + * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. + * */ +export class UserRoleClaimClass extends PrimitiveArrayClaim { + constructor() { + super({ + key: 'st-role', + async fetchValue(userId, userContext) { + const recipe = UserRoleRecipe.getInstanceOrThrowError() + const res = await recipe.recipeInterfaceImpl.getRolesForUser({ + userId, + userContext, + }) + return res.roles + }, + defaultMaxAgeInSeconds: 300, + }) + } +} + +export const UserRoleClaim = new UserRoleClaimClass() diff --git a/src/recipe/userroles/utils.ts b/src/recipe/userroles/utils.ts new file mode 100644 index 000000000..c841d4c5f --- /dev/null +++ b/src/recipe/userroles/utils.ts @@ -0,0 +1,36 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { NormalisedAppinfo } from '../../types' +import Recipe from './recipe' +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from './types' + +export function validateAndNormaliseUserInput( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInput, +): TypeNormalisedInput { + const override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + } + + return { + skipAddingRolesToAccessToken: config?.skipAddingRolesToAccessToken === true, + skipAddingPermissionsToAccessToken: config?.skipAddingPermissionsToAccessToken === true, + override, + } +} diff --git a/src/recipeModule.ts b/src/recipeModule.ts new file mode 100644 index 000000000..71682ebee --- /dev/null +++ b/src/recipeModule.ts @@ -0,0 +1,69 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import STError from './error' +import { APIHandled, HTTPMethod, NormalisedAppinfo } from './types' +import NormalisedURLPath from './normalisedURLPath' +import { BaseRequest } from './framework/request' +import { BaseResponse } from './framework/response' + +export default abstract class RecipeModule { + private recipeId: string + + private appInfo: NormalisedAppinfo + + constructor(recipeId: string, appInfo: NormalisedAppinfo) { + this.recipeId = recipeId + this.appInfo = appInfo + } + + getRecipeId = (): string => { + return this.recipeId + } + + getAppInfo = (): NormalisedAppinfo => { + return this.appInfo + } + + returnAPIIdIfCanHandleRequest = (path: NormalisedURLPath, method: HTTPMethod): string | undefined => { + const apisHandled = this.getAPIsHandled() + for (let i = 0; i < apisHandled.length; i++) { + const currAPI = apisHandled[i] + if ( + !currAPI.disabled + && currAPI.method === method + && this.appInfo.apiBasePath.appendPath(currAPI.pathWithoutApiBasePath).equals(path) + ) + return currAPI.id + } + return undefined + } + + abstract getAPIsHandled(): APIHandled[] + + abstract handleAPIRequest( + id: string, + req: BaseRequest, + response: BaseResponse, + path: NormalisedURLPath, + method: HTTPMethod + ): Promise + + abstract handleError(error: STError, request: BaseRequest, response: BaseResponse): Promise + + abstract getAllCORSHeaders(): string[] + + abstract isErrorFromThisRecipe(err: any): err is STError +} diff --git a/src/supertokens.ts b/src/supertokens.ts new file mode 100644 index 000000000..bdb7021a2 --- /dev/null +++ b/src/supertokens.ts @@ -0,0 +1,416 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { HTTPMethod, NormalisedAppinfo, SuperTokensInfo, TypeInput } from './types' +import { + getRidFromHeader, + maxVersion, + normaliseHttpMethod, + normaliseInputAppInfoOrThrowError, + sendNon200ResponseWithMessage, +} from './utils' +import { Querier } from './querier' +import RecipeModule from './recipeModule' +import { HEADER_FDI, HEADER_RID } from './constants' +import NormalisedURLDomain from './normalisedURLDomain' +import NormalisedURLPath from './normalisedURLPath' +import { BaseRequest, BaseResponse } from './framework' +import { TypeFramework } from './framework/types' +import STError from './error' +import { logDebugMessage } from './logger' +import { PostSuperTokensInitCallbacks } from './postSuperTokensInitCallbacks' + +export default class SuperTokens { + private static instance: SuperTokens | undefined + + framework: TypeFramework + + appInfo: NormalisedAppinfo + + isInServerlessEnv: boolean + + recipeModules: RecipeModule[] + + supertokens: undefined | SuperTokensInfo + + telemetryEnabled: boolean + + constructor(config: TypeInput) { + logDebugMessage('Started SuperTokens with debug logging (supertokens.init called)') + logDebugMessage(`appInfo: ${JSON.stringify(config.appInfo)}`) + + this.framework = config.framework !== undefined ? config.framework : 'express' + logDebugMessage(`framework: ${this.framework}`) + this.appInfo = normaliseInputAppInfoOrThrowError(config.appInfo) + this.supertokens = config.supertokens + + Querier.init( + config.supertokens?.connectionURI + .split(';') + .filter(h => h !== '') + .map((h) => { + return { + domain: new NormalisedURLDomain(h.trim()), + basePath: new NormalisedURLPath(h.trim()), + } + }), + config.supertokens?.apiKey, + ) + if (config.recipeList === undefined || config.recipeList.length === 0) + throw new Error('Please provide at least one recipe to the supertokens.init function call') + + // @ts-expect-error + if (config.recipeList.includes(undefined)) { + // related to issue #270. If user makes mistake by adding empty items in the recipeList, this will catch the mistake and throw relevant error + throw new Error('Please remove empty items from recipeList') + } + + this.isInServerlessEnv = config.isInServerlessEnv === undefined ? false : config.isInServerlessEnv + + this.recipeModules = config.recipeList.map((func) => { + return func(this.appInfo, this.isInServerlessEnv) + }) + + this.telemetryEnabled = config.telemetry === undefined ? process.env.TEST_MODE !== 'testing' : config.telemetry + } + + static init(config: TypeInput) { + if (SuperTokens.instance === undefined) { + SuperTokens.instance = new SuperTokens(config) + PostSuperTokensInitCallbacks.runPostInitCallbacks() + } + } + + static reset() { + if (process.env.TEST_MODE !== 'testing') + throw new Error('calling testing function in non testing env') + + Querier.reset() + SuperTokens.instance = undefined + } + + static getInstanceOrThrowError(): SuperTokens { + if (SuperTokens.instance !== undefined) + return SuperTokens.instance + + throw new Error('Initialisation not done. Did you forget to call the SuperTokens.init function?') + } + + handleAPI = async ( + matchedRecipe: RecipeModule, + id: string, + request: BaseRequest, + response: BaseResponse, + path: NormalisedURLPath, + method: HTTPMethod, + ) => { + return await matchedRecipe.handleAPIRequest(id, request, response, path, method) + } + + getAllCORSHeaders = (): string[] => { + const headerSet = new Set() + headerSet.add(HEADER_RID) + headerSet.add(HEADER_FDI) + this.recipeModules.forEach((recipe) => { + const headers = recipe.getAllCORSHeaders() + headers.forEach((h) => { + headerSet.add(h) + }) + }) + return Array.from(headerSet) + } + + getUserCount = async (includeRecipeIds?: string[]): Promise => { + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') { + throw new Error( + 'Please use core version >= 3.5 to call this function. Otherwise, you can call .getUserCount() instead (for example, EmailPassword.getUserCount())', + ) + } + let includeRecipeIdsStr + if (includeRecipeIds !== undefined) + includeRecipeIdsStr = includeRecipeIds.join(',') + + const response = await querier.sendGetRequest(new NormalisedURLPath('/users/count'), { + includeRecipeIds: includeRecipeIdsStr, + }) + return Number(response.count) + } + + getUsers = async (input: { + timeJoinedOrder: 'ASC' | 'DESC' + limit?: number + paginationToken?: string + includeRecipeIds?: string[] + query?: object + }): Promise<{ + users: { recipeId: string; user: any }[] + nextPaginationToken?: string + }> => { + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') { + throw new Error( + 'Please use core version >= 3.5 to call this function. Otherwise, you can call .getUsersOldestFirst() or .getUsersNewestFirst() instead (for example, EmailPassword.getUsersOldestFirst())', + ) + } + let includeRecipeIdsStr + if (input.includeRecipeIds !== undefined) + includeRecipeIdsStr = input.includeRecipeIds.join(',') + + const response = await querier.sendGetRequest(new NormalisedURLPath('/users'), { + ...input.query, + includeRecipeIds: includeRecipeIdsStr, + timeJoinedOrder: input.timeJoinedOrder, + limit: input.limit, + paginationToken: input.paginationToken, + }) + return { + users: response.users, + nextPaginationToken: response.nextPaginationToken, + } + } + + deleteUser = async (input: { userId: string }): Promise<{ status: 'OK' }> => { + const querier = Querier.getNewInstanceOrThrowError(undefined) + const cdiVersion = await querier.getAPIVersion() + if (maxVersion('2.10', cdiVersion) === cdiVersion) { + // delete user is only available >= CDI 2.10 + await querier.sendPostRequest(new NormalisedURLPath('/user/remove'), { + userId: input.userId, + }) + + return { + status: 'OK', + } + } + else { + throw new global.Error('Please upgrade the SuperTokens core to >= 3.7.0') + } + } + + createUserIdMapping = async function (input: { + superTokensUserId: string + externalUserId: string + externalUserIdInfo?: string + force?: boolean + }): Promise< + | { + status: 'OK' | 'UNKNOWN_SUPERTOKENS_USER_ID_ERROR' + } + | { + status: 'USER_ID_MAPPING_ALREADY_EXISTS_ERROR' + doesSuperTokensUserIdExist: boolean + doesExternalUserIdExist: boolean + } + > { + const querier = Querier.getNewInstanceOrThrowError(undefined) + const cdiVersion = await querier.getAPIVersion() + if (maxVersion('2.15', cdiVersion) === cdiVersion) { + // create userId mapping is only available >= CDI 2.15 + return await querier.sendPostRequest(new NormalisedURLPath('/recipe/userid/map'), { + superTokensUserId: input.superTokensUserId, + externalUserId: input.externalUserId, + externalUserIdInfo: input.externalUserIdInfo, + force: input.force, + }) + } + else { + throw new global.Error('Please upgrade the SuperTokens core to >= 3.15.0') + } + } + + getUserIdMapping = async function (input: { + userId: string + userIdType?: 'SUPERTOKENS' | 'EXTERNAL' | 'ANY' + }): Promise< + | { + status: 'OK' + superTokensUserId: string + externalUserId: string + externalUserIdInfo: string | undefined + } + | { + status: 'UNKNOWN_MAPPING_ERROR' + } + > { + const querier = Querier.getNewInstanceOrThrowError(undefined) + const cdiVersion = await querier.getAPIVersion() + if (maxVersion('2.15', cdiVersion) === cdiVersion) { + // create userId mapping is only available >= CDI 2.15 + const response = await querier.sendGetRequest(new NormalisedURLPath('/recipe/userid/map'), { + userId: input.userId, + userIdType: input.userIdType, + }) + return response + } + else { + throw new global.Error('Please upgrade the SuperTokens core to >= 3.15.0') + } + } + + deleteUserIdMapping = async function (input: { + userId: string + userIdType?: 'SUPERTOKENS' | 'EXTERNAL' | 'ANY' + force?: boolean + }): Promise<{ + status: 'OK' + didMappingExist: boolean + }> { + const querier = Querier.getNewInstanceOrThrowError(undefined) + const cdiVersion = await querier.getAPIVersion() + if (maxVersion('2.15', cdiVersion) === cdiVersion) { + return await querier.sendPostRequest(new NormalisedURLPath('/recipe/userid/map/remove'), { + userId: input.userId, + userIdType: input.userIdType, + force: input.force, + }) + } + else { + throw new global.Error('Please upgrade the SuperTokens core to >= 3.15.0') + } + } + + updateOrDeleteUserIdMappingInfo = async function (input: { + userId: string + userIdType?: 'SUPERTOKENS' | 'EXTERNAL' | 'ANY' + externalUserIdInfo?: string + }): Promise<{ + status: 'OK' | 'UNKNOWN_MAPPING_ERROR' + }> { + const querier = Querier.getNewInstanceOrThrowError(undefined) + const cdiVersion = await querier.getAPIVersion() + if (maxVersion('2.15', cdiVersion) === cdiVersion) { + return await querier.sendPutRequest(new NormalisedURLPath('/recipe/userid/external-user-id-info'), { + userId: input.userId, + userIdType: input.userIdType, + externalUserIdInfo: input.externalUserIdInfo, + }) + } + else { + throw new global.Error('Please upgrade the SuperTokens core to >= 3.15.0') + } + } + + middleware = async (request: BaseRequest, response: BaseResponse): Promise => { + logDebugMessage('middleware: Started') + const path = this.appInfo.apiGatewayPath.appendPath(new NormalisedURLPath(request.getOriginalURL())) + const method: HTTPMethod = normaliseHttpMethod(request.getMethod()) + + // if the prefix of the URL doesn't match the base path, we skip + if (!path.startsWith(this.appInfo.apiBasePath)) { + logDebugMessage( + `middleware: Not handling because request path did not start with config path. Request path: ${ + path.getAsStringDangerous()}`, + ) + return false + } + + let requestRID = getRidFromHeader(request) + logDebugMessage(`middleware: requestRID is: ${requestRID}`) + if (requestRID === 'anti-csrf') { + // see https://github.com/supertokens/supertokens-node/issues/202 + requestRID = undefined + } + if (requestRID !== undefined) { + let matchedRecipe: RecipeModule | undefined + + // we loop through all recipe modules to find the one with the matching rId + for (let i = 0; i < this.recipeModules.length; i++) { + logDebugMessage(`middleware: Checking recipe ID for match: ${this.recipeModules[i].getRecipeId()}`) + if (this.recipeModules[i].getRecipeId() === requestRID) { + matchedRecipe = this.recipeModules[i] + break + } + } + + if (matchedRecipe === undefined) { + logDebugMessage('middleware: Not handling because no recipe matched') + // we could not find one, so we skip + return false + } + logDebugMessage(`middleware: Matched with recipe ID: ${matchedRecipe.getRecipeId()}`) + + const id = matchedRecipe.returnAPIIdIfCanHandleRequest(path, method) + if (id === undefined) { + logDebugMessage( + `middleware: Not handling because recipe doesn't handle request path or method. Request path: ${ + path.getAsStringDangerous() + }, request method: ${ + method}`, + ) + // the matched recipe doesn't handle this path and http method + return false + } + + logDebugMessage(`middleware: Request being handled by recipe. ID is: ${id}`) + + // give task to the matched recipe + const requestHandled = await matchedRecipe.handleAPIRequest(id, request, response, path, method) + if (!requestHandled) { + logDebugMessage('middleware: Not handled because API returned requestHandled as false') + return false + } + logDebugMessage('middleware: Ended') + return true + } + else { + // we loop through all recipe modules to find the one with the matching path and method + for (let i = 0; i < this.recipeModules.length; i++) { + logDebugMessage(`middleware: Checking recipe ID for match: ${this.recipeModules[i].getRecipeId()}`) + const id = this.recipeModules[i].returnAPIIdIfCanHandleRequest(path, method) + if (id !== undefined) { + logDebugMessage(`middleware: Request being handled by recipe. ID is: ${id}`) + const requestHandled = await this.recipeModules[i].handleAPIRequest( + id, + request, + response, + path, + method, + ) + if (!requestHandled) { + logDebugMessage('middleware: Not handled because API returned requestHandled as false') + return false + } + logDebugMessage('middleware: Ended') + return true + } + } + logDebugMessage('middleware: Not handling because no recipe matched') + return false + } + } + + errorHandler = async (err: any, request: BaseRequest, response: BaseResponse) => { + logDebugMessage('errorHandler: Started') + if (STError.isErrorFromSuperTokens(err)) { + logDebugMessage(`errorHandler: Error is from SuperTokens recipe. Message: ${err.message}`) + if (err.type === STError.BAD_INPUT_ERROR) { + logDebugMessage('errorHandler: Sending 400 status code response') + return sendNon200ResponseWithMessage(response, err.message, 400) + } + + for (let i = 0; i < this.recipeModules.length; i++) { + logDebugMessage(`errorHandler: Checking recipe for match: ${this.recipeModules[i].getRecipeId()}`) + if (this.recipeModules[i].isErrorFromThisRecipe(err)) { + logDebugMessage(`errorHandler: Matched with recipeID: ${this.recipeModules[i].getRecipeId()}`) + return await this.recipeModules[i].handleError(err, request, response) + } + } + } + throw err + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..35c6f067a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,75 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import RecipeModule from './recipeModule' +import NormalisedURLDomain from './normalisedURLDomain' +import NormalisedURLPath from './normalisedURLPath' +import { TypeFramework } from './framework/types' + +export interface AppInfo { + appName: string + websiteDomain: string + websiteBasePath?: string + apiDomain: string + apiBasePath?: string + apiGatewayPath?: string +} + +export interface NormalisedAppinfo { + appName: string + websiteDomain: NormalisedURLDomain + apiDomain: NormalisedURLDomain + topLevelAPIDomain: string + topLevelWebsiteDomain: string + apiBasePath: NormalisedURLPath + apiGatewayPath: NormalisedURLPath + websiteBasePath: NormalisedURLPath +} + +export interface SuperTokensInfo { + connectionURI: string + apiKey?: string +} + +export interface TypeInput { + supertokens?: SuperTokensInfo + framework?: TypeFramework + appInfo: AppInfo + recipeList: RecipeListFunction[] + telemetry?: boolean + isInServerlessEnv?: boolean +} + +export type RecipeListFunction = (appInfo: NormalisedAppinfo, isInServerlessEnv: boolean) => RecipeModule + +export interface APIHandled { + pathWithoutApiBasePath: NormalisedURLPath + method: HTTPMethod + id: string + disabled: boolean +} + +export type HTTPMethod = 'post' | 'get' | 'delete' | 'put' | 'options' | 'trace' + +export type JSONPrimitive = string | number | boolean | null +export type JSONArray = Array +export type JSONValue = JSONPrimitive | JSONObject | JSONArray | undefined +export interface JSONObject { + [ind: string]: JSONValue +} +export interface GeneralErrorResponse { + status: 'GENERAL_ERROR' + message: string +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..d6d93bfdf --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,172 @@ +import * as psl from 'psl' + +import { HEADER_RID } from './constants' +import type { AppInfo, HTTPMethod, JSONObject, NormalisedAppinfo } from './types' +import NormalisedURLDomain from './normalisedURLDomain' +import NormalisedURLPath from './normalisedURLPath' +import { logDebugMessage } from './logger' +import type { BaseResponse } from './framework/response' +import type { BaseRequest } from './framework/request' + +export function getLargestVersionFromIntersection(v1: string[], v2: string[]): string | undefined { + const intersection = v1.filter(value => v2.includes(value)) + if (intersection.length === 0) + return undefined + + let maxVersionSoFar = intersection[0] + for (let i = 1; i < intersection.length; i++) + maxVersionSoFar = maxVersion(intersection[i], maxVersionSoFar) + + return maxVersionSoFar +} + +export function maxVersion(version1: string, version2: string): string { + const splittedv1 = version1.split('.') + const splittedv2 = version2.split('.') + const minLength = Math.min(splittedv1.length, splittedv2.length) + for (let i = 0; i < minLength; i++) { + const v1 = Number(splittedv1[i]) + const v2 = Number(splittedv2[i]) + if (v1 > v2) + return version1 + else if (v2 > v1) + return version2 + } + if (splittedv1.length >= splittedv2.length) + return version1 + + return version2 +} + +export function normaliseInputAppInfoOrThrowError(appInfo: AppInfo): NormalisedAppinfo { + if (appInfo === undefined) + throw new Error('Please provide the appInfo object when calling supertokens.init') + + if (appInfo.apiDomain === undefined) + throw new Error('Please provide your apiDomain inside the appInfo object when calling supertokens.init') + + if (appInfo.appName === undefined) + throw new Error('Please provide your appName inside the appInfo object when calling supertokens.init') + + if (appInfo.websiteDomain === undefined) + throw new Error('Please provide your websiteDomain inside the appInfo object when calling supertokens.init') + + const apiGatewayPath + = appInfo.apiGatewayPath !== undefined + ? new NormalisedURLPath(appInfo.apiGatewayPath) + : new NormalisedURLPath('') + + const websiteDomain = new NormalisedURLDomain(appInfo.websiteDomain) + const apiDomain = new NormalisedURLDomain(appInfo.apiDomain) + const topLevelAPIDomain = getTopLevelDomainForSameSiteResolution(apiDomain.getAsStringDangerous()) + const topLevelWebsiteDomain = getTopLevelDomainForSameSiteResolution(websiteDomain.getAsStringDangerous()) + + return { + appName: appInfo.appName, + websiteDomain, + apiDomain, + apiBasePath: apiGatewayPath.appendPath( + appInfo.apiBasePath === undefined + ? new NormalisedURLPath('/auth') + : new NormalisedURLPath(appInfo.apiBasePath), + ), + websiteBasePath: + appInfo.websiteBasePath === undefined + ? new NormalisedURLPath('/auth') + : new NormalisedURLPath(appInfo.websiteBasePath), + apiGatewayPath, + topLevelAPIDomain, + topLevelWebsiteDomain, + } +} + +export function normaliseHttpMethod(method: string): HTTPMethod { + return method.toLowerCase() as HTTPMethod +} + +export function sendNon200ResponseWithMessage(res: BaseResponse, message: string, statusCode: number) { + sendNon200Response(res, statusCode, { message }) +} + +export function sendNon200Response(res: BaseResponse, statusCode: number, body: JSONObject) { + if (statusCode < 300) + throw new Error('Calling sendNon200Response with status code < 300') + + logDebugMessage(`Sending response to client with status code: ${statusCode}`) + res.setStatusCode(statusCode) + res.sendJSONResponse(body) +} + +export function send200Response(res: BaseResponse, responseJson: any) { + logDebugMessage('Sending response to client with status code: 200') + res.setStatusCode(200) + res.sendJSONResponse(responseJson) +} + +export function isAnIpAddress(ipaddress: string) { + return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( + ipaddress, + ) +} + +export function getRidFromHeader(req: BaseRequest): string | undefined { + return req.getHeaderValue(HEADER_RID) +} + +export function frontendHasInterceptor(req: BaseRequest): boolean { + return getRidFromHeader(req) !== undefined +} + +export function humaniseMilliseconds(ms: number): string { + const t = Math.floor(ms / 1000) + let suffix = '' + + if (t < 60) { + if (t > 1) + suffix = 's' + return `${t} second${suffix}` + } + else if (t < 3600) { + const m = Math.floor(t / 60) + if (m > 1) + suffix = 's' + return `${m} minute${suffix}` + } + else { + const h = Math.floor(t / 360) / 10 + if (h > 1) + suffix = 's' + return `${h} hour${suffix}` + } +} + +export function makeDefaultUserContextFromAPI(request: BaseRequest): any { + return { + _default: { + request, + }, + } +} + +export function getTopLevelDomainForSameSiteResolution(url: string): string { + const urlObj = new URL(url) + const hostname = urlObj.hostname + if (hostname.startsWith('localhost') || hostname.startsWith('localhost.org') || isAnIpAddress(hostname)) { + // we treat these as the same TLDs since we can use sameSite lax for all of them. + return 'localhost' + } + const parsedURL = psl.parse(hostname) as psl.ParsedDomain + if (parsedURL.domain === null) + throw new Error('Please make sure that the apiDomain and websiteDomain have correct values') + + return parsedURL.domain +} + +export function getFromObjectCaseInsensitive(key: string, object: Record): T | undefined { + const matchedKeys = Object.keys(object).filter(i => i.toLocaleLowerCase() === key.toLocaleLowerCase()) + + if (matchedKeys.length === 0) + return undefined + + return object[matchedKeys[0]] +} diff --git a/lib/ts/version.ts b/src/version.ts similarity index 77% rename from lib/ts/version.ts rename to src/version.ts index 8c14438c1..a56b68a08 100644 --- a/lib/ts/version.ts +++ b/src/version.ts @@ -12,23 +12,23 @@ * License for the specific language governing permissions and limitations * under the License. */ -export const version = "13.6.0"; +export const version = '13.6.0' export const cdiSupported = [ - "2.8", - "2.9", - "2.10", - "2.11", - "2.12", - "2.13", - "2.14", - "2.15", - "2.16", - "2.17", - "2.18", - "2.19", - "2.20", -]; + '2.8', + '2.9', + '2.10', + '2.11', + '2.12', + '2.13', + '2.14', + '2.15', + '2.16', + '2.17', + '2.18', + '2.19', + '2.20', +] // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} -export const dashboardVersion = "0.6"; +export const dashboardVersion = '0.6' diff --git a/test/auth-modes.test.js b/test/auth-modes.test.js deleted file mode 100644 index 1011ab08e..000000000 --- a/test/auth-modes.test.js +++ /dev/null @@ -1,1139 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, - delay, -} = require("./utils"); -const assert = require("assert"); -const { ProcessState } = require("../lib/build/processState"); -const SuperTokens = require("../"); -const Session = require("../recipe/session"); -const { verifySession } = require("../recipe/session/framework/express"); -const { middleware, errorHandler } = require("../framework/express"); -const express = require("express"); -const request = require("supertest"); -const sinon = require("sinon"); - -const exampleJWT = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - -describe(`auth-modes: ${printPath("[test/auth-modes.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - afterEach(function () { - sinon.restore(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("with default getTokenTransferMethod", () => { - describe("createNewSession", () => { - describe("with default getTokenTransferMethod", () => { - it("should default to header based session w/ no auth-mode header", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN" })], - }); - - const app = getTestApp(); - - const resp = await createSession(app); - assert.strictEqual(resp.accessToken, undefined); - assert.strictEqual(resp.refreshToken, undefined); - assert.strictEqual(resp.antiCsrf, undefined); - assert.notStrictEqual(resp.accessTokenFromHeader, undefined); - assert.notStrictEqual(resp.refreshTokenFromHeader, undefined); - }); - - it("should default to header based session w/ bad auth-mode header", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN" })], - }); - - const app = getTestApp(); - - const resp = await createSession(app, "lol"); - assert.strictEqual(resp.accessToken, undefined); - assert.strictEqual(resp.refreshToken, undefined); - assert.strictEqual(resp.antiCsrf, undefined); - assert.notStrictEqual(resp.accessTokenFromHeader, undefined); - assert.notStrictEqual(resp.refreshTokenFromHeader, undefined); - }); - - it("should use headers if auth-mode specifies it", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN" })], - }); - - const app = getTestApp(); - - const resp = await createSession(app, "header"); - assert.strictEqual(resp.accessToken, undefined); - assert.strictEqual(resp.refreshToken, undefined); - assert.strictEqual(resp.antiCsrf, undefined); - assert.notStrictEqual(resp.accessTokenFromHeader, undefined); - assert.notStrictEqual(resp.refreshTokenFromHeader, undefined); - }); - - it("should use cookies if auth-mode specifies it", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN" })], - }); - - const app = getTestApp(); - - const resp = await createSession(app, "cookie"); - assert.notStrictEqual(resp.accessToken, undefined); - assert.notStrictEqual(resp.refreshToken, undefined); - assert.notStrictEqual(resp.antiCsrf, undefined); - assert.strictEqual(resp.accessTokenFromHeader, undefined); - assert.strictEqual(resp.refreshTokenFromHeader, undefined); - - // We check that we will have access token at least as long as we have a refresh token - // so verify session can return TRY_REFRESH_TOKEN - assert(Date.parse(resp.accessTokenExpiry) >= Date.parse(resp.refreshTokenExpiry)); - }); - }); - - describe("with user provided getTokenTransferMethod", () => { - it("should use headers if getTokenTransferMethod returns any", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN", getTokenTransferMethod: () => "any" })], - }); - - const app = getTestApp(); - - const resp = await createSession(app, "cookie"); - assert.strictEqual(resp.accessToken, undefined); - assert.strictEqual(resp.refreshToken, undefined); - assert.strictEqual(resp.antiCsrf, undefined); - assert.notStrictEqual(resp.accessTokenFromHeader, undefined); - assert.notStrictEqual(resp.refreshTokenFromHeader, undefined); - }); - - it("should use headers if getTokenTransferMethod returns header", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN", getTokenTransferMethod: () => "header" })], - }); - - const app = getTestApp(); - - const resp = await createSession(app, "cookie"); - assert.strictEqual(resp.accessToken, undefined); - assert.strictEqual(resp.refreshToken, undefined); - assert.strictEqual(resp.antiCsrf, undefined); - assert.notStrictEqual(resp.accessTokenFromHeader, undefined); - assert.notStrictEqual(resp.refreshTokenFromHeader, undefined); - }); - - it("should use clear cookies (if present) if getTokenTransferMethod returns header", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN", getTokenTransferMethod: () => "header" })], - }); - - const app = getTestApp(); - - const resp = extractInfoFromResponse( - await new Promise((resolve, reject) => { - const req = request(app).post("/create"); - req.set("st-auth-mode", "header"); - return req - .set("Cookie", ["sAccessToken=" + exampleJWT]) - .send(undefined) - .expect(200) - .end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }) - ); - assert.strictEqual(resp.accessToken, ""); - assert.strictEqual(resp.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(resp.refreshToken, ""); - assert.strictEqual(resp.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(resp.antiCsrf, undefined); - }); - - it("should use cookies if getTokenTransferMethod returns cookie", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN", getTokenTransferMethod: () => "cookie" })], - }); - - const app = getTestApp(); - - const resp = await createSession(app, "header"); - assert.notStrictEqual(resp.accessToken, undefined); - assert.notStrictEqual(resp.refreshToken, undefined); - assert.notStrictEqual(resp.antiCsrf, undefined); - assert.strictEqual(resp.accessTokenFromHeader, undefined); - assert.strictEqual(resp.refreshTokenFromHeader, undefined); - - // We check that we will have access token at least as long as we have a refresh token - // so verify session can return TRY_REFRESH_TOKEN - assert(Date.parse(resp.accessTokenExpiry) >= Date.parse(resp.refreshTokenExpiry)); - }); - - it("should clear headers (if present) if getTokenTransferMethod returns cookie", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN", getTokenTransferMethod: () => "cookie" })], - }); - - const app = getTestApp(); - - const resp = extractInfoFromResponse( - await new Promise((resolve, reject) => { - const req = request(app).post("/create"); - req.set("st-auth-mode", "header"); - return req - .set("Authorization", `Bearer ${exampleJWT}`) - .send(undefined) - .expect(200) - .end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }) - ); - - assert.strictEqual(resp.accessTokenFromHeader, ""); - assert.strictEqual(resp.refreshTokenFromHeader, ""); - }); - }); - }); - - describe("verifySession", () => { - describe("from behaviour table", () => { - // prettier-ignore - const behaviourTable = [ - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: false, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: true, authCookie: false, output: "undefined" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: false, authCookie: true, output: "undefined" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: false, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: true, authCookie: false, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: false, authCookie: true, output: "UNAUTHORISED" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: true, authCookie: true, output: "validateheader" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: true, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: true, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: true, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "header", sessionRequired: false, authHeader: true, authCookie: false, output: "validateheader" }, - { getTokenTransferMethodRes: "any", sessionRequired: true, authHeader: false, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "any", sessionRequired: false, authHeader: false, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: true, authHeader: false, authCookie: true, output: "validatecookie" }, - { getTokenTransferMethodRes: "cookie", sessionRequired: false, authHeader: false, authCookie: true, output: "validatecookie" }, - ]; - - for (let i = 0; i < behaviourTable.length; ++i) { - const conf = behaviourTable[i]; - it(`should match line ${i + 1} with a valid token`, async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => conf.getTokenTransferMethodRes, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfo = await createSession(app, "cookie"); - - const authMode = - conf.authCookie && conf.authHeader - ? "both" - : conf.authCookie - ? "cookie" - : conf.authHeader - ? "header" - : "none"; - - const res = await testGet( - app, - createInfo, - conf.sessionRequired ? "/verify" : "/verify-optional", - conf.output === "UNAUTHORISED" ? 401 : 200, - authMode - ); - switch (conf.output) { - case "undefined": - assert.strictEqual(res.body.sessionExists, false); - break; - case "UNAUTHORISED": - assert.deepStrictEqual(res.body, { message: "unauthorised" }); - break; - case "validatecookie": - case "validateheader": - assert.strictEqual(res.body.sessionExists, true); - } - }); - - it(`should match line ${i + 1} with a expired token`, async () => { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => conf.getTokenTransferMethodRes, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfo = await createSession(app, "cookie"); - await delay(3); - const authMode = - conf.authCookie && conf.authHeader - ? "both" - : conf.authCookie - ? "cookie" - : conf.authHeader - ? "header" - : "none"; - - const res = await testGet( - app, - createInfo, - conf.sessionRequired ? "/verify" : "/verify-optional", - conf.output !== "undefined" ? 401 : 200, - authMode - ); - switch (conf.output) { - case "undefined": - assert.strictEqual(res.body.sessionExists, false); - break; - case "UNAUTHORISED": - assert.deepStrictEqual(res.body, { message: "unauthorised" }); - break; - case "validatecookie": - case "validateheader": - assert.deepStrictEqual(res.body, { message: "try refresh token" }); - } - }); - } - }); - - describe("with access tokens in both headers and cookies", () => { - it("should use the value from headers if getTokenTransferMethod returns any", async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "any", - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfoCookie = await createSession(app, "header"); - const createInfoHeader = await createSession(app, "header"); - - const resp = await new Promise((resolve, reject) => { - const req = request(app).get("/verify"); - - req.set("Cookie", ["sAccessToken=" + createInfoCookie.accessTokenFromHeader]); - if (createInfoCookie.antiCsrf) { - req.set("anti-csrf", info.antiCsrf); - } - req.set( - "Authorization", - `Bearer ${decodeURIComponent(createInfoHeader.accessTokenFromHeader)}` - ); - req.expect(200).end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); - - assert.strictEqual(resp.body.sessionHandle, createInfoHeader.body.sessionHandle); - }); - - it("should use the value from headers if getTokenTransferMethod returns header", async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: ({ forCreateNewSession }) => - forCreateNewSession ? "any" : "header", - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfoCookie = await createSession(app, "header"); - const createInfoHeader = await createSession(app, "header"); - - const resp = await new Promise((resolve, reject) => { - const req = request(app).get("/verify"); - - req.set("Cookie", ["sAccessToken=" + createInfoCookie.accessTokenFromHeader]); - if (createInfoCookie.antiCsrf) { - req.set("anti-csrf", info.antiCsrf); - } - req.set( - "Authorization", - `Bearer ${decodeURIComponent(createInfoHeader.accessTokenFromHeader)}` - ); - req.expect(200).end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); - - assert.strictEqual(resp.body.sessionHandle, createInfoHeader.body.sessionHandle); - }); - - it("should use the value from cookies if getTokenTransferMethod returns cookie", async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: ({ forCreateNewSession }) => - forCreateNewSession ? "any" : "cookie", - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfoCookie = await createSession(app, "header"); - const createInfoHeader = await createSession(app, "header"); - - const resp = await new Promise((resolve, reject) => { - const req = request(app).get("/verify"); - - req.set("Cookie", ["sAccessToken=" + createInfoCookie.accessTokenFromHeader]); - if (createInfoCookie.antiCsrf) { - req.set("anti-csrf", info.antiCsrf); - } - req.set( - "Authorization", - `Bearer ${decodeURIComponent(createInfoHeader.accessTokenFromHeader)}` - ); - req.expect(200).end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); - - assert.strictEqual(resp.body.sessionHandle, createInfoCookie.body.sessionHandle); - }); - }); - - it("should reject requests with sIdRefreshToken", async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfo = await createSession(app, "cookie"); - - const res = await new Promise((resolve, reject) => - request(app) - .get("/verify") - .set("Cookie", [ - "sAccessToken=" + createInfo.accessToken, - "sIdRefreshToken=" + createInfo.refreshToken, // The value doesn't actually matter - ]) - .set("anti-csrf", createInfo.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res.status, 401); - assert.deepStrictEqual(res.body, { message: "try refresh token" }); - }); - - describe("with non ST in Authorize header", () => { - it("should use the value from cookies if present and getTokenTransferMethod returns any", async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "any", - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfoCookie = await createSession(app, "header"); - - const resp = await new Promise((resolve, reject) => { - const req = request(app).get("/verify"); - - req.set("Cookie", ["sAccessToken=" + createInfoCookie.accessTokenFromHeader]); - if (createInfoCookie.antiCsrf) { - req.set("anti-csrf", info.antiCsrf); - } - req.set("Authorization", `Bearer ${exampleJWT}`); - req.expect(200).end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); - - assert.strictEqual(resp.body.sessionHandle, createInfoCookie.body.sessionHandle); - }); - - it("should reject with UNAUTHORISED if getTokenTransferMethod returns header", async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: ({ forCreateNewSession }) => - forCreateNewSession ? "any" : "header", - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const createInfoCookie = await createSession(app, "header"); - - const resp = await new Promise((resolve, reject) => { - const req = request(app).get("/verify"); - - req.set("Cookie", ["sAccessToken=" + createInfoCookie.accessTokenFromHeader]); - if (createInfoCookie.antiCsrf) { - req.set("anti-csrf", info.antiCsrf); - } - req.set("Authorization", `Bearer ${exampleJWT}`); - req.end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); - assert.strictEqual(resp.status, 401); - assert.deepStrictEqual(resp.body, { message: "unauthorised" }); - }); - - it("should reject with UNAUTHORISED if cookies are not present", async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: ({}) => "any", - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const resp = await new Promise((resolve, reject) => { - const req = request(app).get("/verify"); - - req.set("Authorization", `Bearer ${exampleJWT}`); - req.end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); - - assert.strictEqual(resp.status, 401); - assert.deepStrictEqual(resp.body, { message: "unauthorised" }); - }); - }); - }); - - describe("mergeIntoAccessTokenPayload", () => { - it("should update cookies if the session was cookie based", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN" })], - }); - - const app = getTestApp(); - - const createInfo = await createSession(app, "header"); - - const updateInfo = extractInfoFromResponse( - await testGet(app, createInfo, "/update-payload", 200, "cookie", undefined) - ); - - // Didn't update - assert.strictEqual(updateInfo.refreshToken, undefined); - assert.strictEqual(updateInfo.antiCsrf, undefined); - assert.strictEqual(updateInfo.accessTokenFromHeader, undefined); - assert.strictEqual(updateInfo.refreshTokenFromHeader, undefined); - - // Updated access token - assert.notStrictEqual(updateInfo.accessToken, undefined); - assert.notStrictEqual(updateInfo.accessToken, createInfo.accessTokenFromHeader); - - // Updated front token - assert.notStrictEqual(updateInfo.frontToken, undefined); - assert.notStrictEqual(updateInfo.frontToken, createInfo.frontToken); - }); - - it("should allow headers if the session was header based", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ antiCsrf: "VIA_TOKEN" })], - }); - - const app = getTestApp(); - - const createInfo = await createSession(app, "cookie"); - - const updateInfo = extractInfoFromResponse( - await testGet(app, createInfo, "/update-payload", 200, "header", undefined) - ); - - // Didn't update - assert.strictEqual(updateInfo.accessToken, undefined); - assert.strictEqual(updateInfo.refreshToken, undefined); - assert.strictEqual(updateInfo.antiCsrf, undefined); - assert.strictEqual(updateInfo.refreshTokenFromHeader, undefined); - - // Updated access token - assert.notStrictEqual(updateInfo.accessTokenFromHeader, undefined); - assert.notStrictEqual(updateInfo.accessTokenFromHeader, createInfo.accessToken); - - // Updated front token - assert.notStrictEqual(updateInfo.frontToken, undefined); - assert.notStrictEqual(updateInfo.frontToken, createInfo.frontToken); - }); - }); - - describe("refreshSession", () => { - describe("from behaviour table", () => { - // prettier-ignore - const behaviourTable = [ - { getTokenTransferMethodRes: "any", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, - { getTokenTransferMethodRes: "header", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, - { getTokenTransferMethodRes: "cookie", authHeader: false, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, - { getTokenTransferMethodRes: "any", authHeader: false, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "none" }, - { getTokenTransferMethodRes: "header", authHeader: false, authCookie: true, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, // 5 - { getTokenTransferMethodRes: "cookie", authHeader: false, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "none" }, - { getTokenTransferMethodRes: "any", authHeader: true, authCookie: false, output: "validateheader", setTokens: "headers", clearedTokens: "none" }, - { getTokenTransferMethodRes: "header", authHeader: true, authCookie: false, output: "validateheader", setTokens: "headers", clearedTokens: "none" }, - { getTokenTransferMethodRes: "cookie", authHeader: true, authCookie: false, output: "unauthorised", setTokens: "none", clearedTokens: "none" }, // 9 - { getTokenTransferMethodRes: "any", authHeader: true, authCookie: true, output: "validateheader", setTokens: "headers", clearedTokens: "cookies" }, - { getTokenTransferMethodRes: "header", authHeader: true, authCookie: true, output: "validateheader", setTokens: "headers", clearedTokens: "cookies" }, - { getTokenTransferMethodRes: "cookie", authHeader: true, authCookie: true, output: "validatecookie", setTokens: "cookies", clearedTokens: "headers" }, // 12 - ]; - - for (let i = 0; i < behaviourTable.length; ++i) { - const conf = behaviourTable[i]; - it(`should match line ${i + 1} with a valid token`, async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => conf.getTokenTransferMethodRes, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - // Which we create doesn't really matter, since the token is the same - const createInfo = await createSession(app, "header"); - - const authMode = - conf.authCookie && conf.authHeader - ? "both" - : conf.authCookie - ? "cookie" - : conf.authHeader - ? "header" - : "none"; - - const refreshRes = await refreshSession( - app, - conf.getTokenTransferMethodRes, - authMode, - createInfo - ); - - if (conf.output === "unauthorised") { - assert.strictEqual(refreshRes.status, 401); - assert.deepStrictEqual(refreshRes.body, { message: "unauthorised" }); - } else { - assert.strictEqual(refreshRes.status, 200); - } - - if (conf.clearedTokens === "headers") { - assert.strictEqual(refreshRes.accessTokenFromHeader, ""); - assert.strictEqual(refreshRes.refreshTokenFromHeader, ""); - } else if (conf.clearedTokens === "cookies") { - assert.strictEqual(refreshRes.accessToken, ""); - assert.strictEqual(refreshRes.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(refreshRes.refreshToken, ""); - assert.strictEqual(refreshRes.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - } - - switch (conf.setTokens) { - case "headers": - assert.ok(refreshRes.accessTokenFromHeader); - assert.notStrictEqual(refreshRes.accessTokenFromHeader, ""); - assert.ok(refreshRes.refreshTokenFromHeader); - assert.notStrictEqual(refreshRes.refreshTokenFromHeader, ""); - break; - case "cookies": - assert.notStrictEqual(refreshRes.accessToken, ""); - assert.notStrictEqual(refreshRes.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.notStrictEqual(refreshRes.refreshToken, ""); - assert.notStrictEqual(refreshRes.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - break; - case "none": - if (conf.clearedTokens === "none") { - assert.strictEqual(refreshRes.frontToken, undefined); - } - break; - } - if (conf.setTokens !== "cookies" && conf.clearedTokens !== "cookies") { - assert.strictEqual(refreshRes.accessToken, undefined); - assert.strictEqual(refreshRes.accessTokenExpiry, undefined); - assert.strictEqual(refreshRes.refreshToken, undefined); - assert.strictEqual(refreshRes.refreshTokenExpiry, undefined); - } - if (conf.setTokens !== "headers" && conf.clearedTokens !== "headers") { - assert.strictEqual(refreshRes.accessTokenFromHeader, undefined); - assert.strictEqual(refreshRes.refreshTokenFromHeader, undefined); - } - }); - } - - for (let i = 0; i < behaviourTable.length; ++i) { - const conf = behaviourTable[i]; - - it(`should match line ${i + 1} with a invalid token`, async () => { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => conf.getTokenTransferMethodRes, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const infoWithInvalidRefreshToken = { - refreshToken: "asdf", - }; - - const authMode = - conf.authCookie && conf.authHeader - ? "both" - : conf.authCookie - ? "cookie" - : conf.authHeader - ? "header" - : "none"; - - const refreshRes = await refreshSession( - app, - conf.getTokenTransferMethodRes, - authMode, - infoWithInvalidRefreshToken - ); - - assert.strictEqual(refreshRes.status, 401); - assert.deepStrictEqual(refreshRes.body, { message: "unauthorised" }); - if (conf.output === "validateheader") { - assert.strictEqual(refreshRes.accessTokenFromHeader, ""); - assert.strictEqual(refreshRes.refreshTokenFromHeader, ""); - } else if (conf.output === "validatecookie") { - assert.strictEqual(refreshRes.accessToken, ""); - assert.strictEqual(refreshRes.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(refreshRes.refreshToken, ""); - assert.strictEqual(refreshRes.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - } - }); - } - }); - }); - }); -}); - -async function createSession(app, authModeHeader, body) { - return extractInfoFromResponse( - await new Promise((resolve, reject) => { - const req = request(app).post("/create"); - if (authModeHeader) { - req.set("st-auth-mode", authModeHeader); - } - return req - .send(body) - .expect(200) - .end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }) - ); -} - -async function refreshSession(app, authModeHeader, authMode, info) { - return extractInfoFromResponse( - await new Promise((resolve, reject) => { - const req = request(app).post("/auth/session/refresh"); - if (authModeHeader) { - req.set("st-auth-mode", authModeHeader); - } - - const accessToken = info.accessToken || info.accessTokenFromHeader; - const refreshToken = info.refreshToken || info.refreshTokenFromHeader; - - if (authMode === "both" || authMode === "cookie") { - req.set("Cookie", ["sAccessToken=" + accessToken, "sRefreshToken=" + refreshToken]); - if (info.antiCsrf) { - req.set("anti-csrf", info.antiCsrf); - } - } - if (authMode === "both" || authMode === "header") { - req.set("Authorization", `Bearer ${decodeURIComponent(refreshToken)}`); - } - return req.send().end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }) - ); -} - -function testGet(app, info, url, expectedStatus, authMode, authModeHeader) { - return new Promise((resolve, reject) => { - const req = request(app).get(url); - const accessToken = info.accessToken || info.accessTokenFromHeader; - - if (authModeHeader) { - req.set("st-auth-mode", authModeHeader); - } - if (authMode === "cookie" || authMode === "both") { - req.set("Cookie", ["sAccessToken=" + accessToken]); - if (info.antiCsrf) { - req.set("anti-csrf", info.antiCsrf); - } - } - if (authMode === "header" || authMode === "both") { - req.set("Authorization", `Bearer ${decodeURIComponent(accessToken)}`); - } - return req.expect(expectedStatus).end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); - }); -} - -function getTestApp(endpoints) { - const app = express(); - - app.use(middleware()); - - app.use(express.json()); - - app.post("/create", async (req, res) => { - const session = await Session.createNewSession(req, res, "testing-userId", req.body, {}); - res.status(200).json({ message: true, sessionHandle: session.getHandle() }); - }); - - app.get("/update-payload", verifySession(), async (req, res) => { - await req.session.mergeIntoAccessTokenPayload({ newValue: "test" }); - res.status(200).json({ message: true }); - }); - - app.get("/verify", verifySession(), async (req, res) => { - res.status(200).json({ message: true, sessionHandle: req.session.getHandle(), sessionExists: true }); - }); - - app.get("/verify-optional", verifySession({ sessionRequired: false }), async (req, res) => { - res.status(200).json({ - message: true, - sessionHandle: req.session && req.session.getHandle(), - sessionExists: !!req.session, - }); - }); - - if (endpoints !== undefined) { - for (const { path, overrideGlobalClaimValidators } of endpoints) { - app.get(path, verifySession({ overrideGlobalClaimValidators }), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - } - } - - app.post("/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - return app; -} diff --git a/test/auth-modes.test.ts b/test/auth-modes.test.ts new file mode 100644 index 000000000..7c68bae5f --- /dev/null +++ b/test/auth-modes.test.ts @@ -0,0 +1,1142 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import assert from 'assert' +import express from 'express' +import request from 'supertest' +import sinon from 'sinon' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { verifySession } from 'supertokens-node/recipe/session/framework/express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' + +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { + cleanST, + delay, + extractInfoFromResponse, + killAllST, + printPath, + setKeyValueInConfig, + setupST, + startST, +} from './utils' + +const exampleJWT + = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + +async function createSession(app: any, authModeHeader: any, body: any) { + return extractInfoFromResponse( + await new Promise((resolve, reject) => { + const req = request(app).post('/create') + if (authModeHeader) + req.set('st-auth-mode', authModeHeader) + + return req + .send(body) + .expect(200) + .end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }), + ) +} + +async function refreshSession(app, authModeHeader, authMode, info) { + return extractInfoFromResponse( + await new Promise((resolve, reject) => { + const req = request(app).post('/auth/session/refresh') + if (authModeHeader) + req.set('st-auth-mode', authModeHeader) + + const accessToken = info.accessToken || info.accessTokenFromHeader + const refreshToken = info.refreshToken || info.refreshTokenFromHeader + + if (authMode === 'both' || authMode === 'cookie') { + req.set('Cookie', [`sAccessToken=${accessToken}`, `sRefreshToken=${refreshToken}`]) + if (info.antiCsrf) + req.set('anti-csrf', info.antiCsrf) + } + if (authMode === 'both' || authMode === 'header') + req.set('Authorization', `Bearer ${decodeURIComponent(refreshToken)}`) + + return req.send().end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }), + ) +} + +function testGet(app, info, url, expectedStatus, authMode, authModeHeader) { + return new Promise((resolve, reject) => { + const req = request(app).get(url) + const accessToken = info.accessToken || info.accessTokenFromHeader + + if (authModeHeader) + req.set('st-auth-mode', authModeHeader) + + if (authMode === 'cookie' || authMode === 'both') { + req.set('Cookie', [`sAccessToken=${accessToken}`]) + if (info.antiCsrf) + req.set('anti-csrf', info.antiCsrf) + } + if (authMode === 'header' || authMode === 'both') + req.set('Authorization', `Bearer ${decodeURIComponent(accessToken)}`) + + return req.expect(expectedStatus).end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }) +} + +function getTestApp(endpoints: any) { + const app = express() + + app.use(middleware()) + + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'testing-userId', req.body, {}) + res.status(200).json({ message: true, sessionHandle: session.getHandle() }) + }) + + app.get('/update-payload', verifySession(), async (req, res) => { + await req.session.mergeIntoAccessTokenPayload({ newValue: 'test' }) + res.status(200).json({ message: true }) + }) + + app.get('/verify', verifySession(), async (req, res) => { + res.status(200).json({ message: true, sessionHandle: req.session.getHandle(), sessionExists: true }) + }) + + app.get('/verify-optional', verifySession({ sessionRequired: false }), async (req, res) => { + res.status(200).json({ + message: true, + sessionHandle: req.session && req.session.getHandle(), + sessionExists: !!req.session, + }) + }) + + if (endpoints !== undefined) { + for (const { path, overrideGlobalClaimValidators } of endpoints) { + app.get(path, verifySession({ overrideGlobalClaimValidators }), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + } + } + + app.post('/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + return app +} + +describe(`auth-modes: ${printPath('[test/auth-modes.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterEach(() => { + sinon.restore() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('with default getTokenTransferMethod', () => { + describe('createNewSession', () => { + describe('with default getTokenTransferMethod', () => { + it('should default to header based session w/ no auth-mode header', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN' })], + }) + + const app = getTestApp() + + const resp = await createSession(app) + assert.strictEqual(resp.accessToken, undefined) + assert.strictEqual(resp.refreshToken, undefined) + assert.strictEqual(resp.antiCsrf, undefined) + assert.notStrictEqual(resp.accessTokenFromHeader, undefined) + assert.notStrictEqual(resp.refreshTokenFromHeader, undefined) + }) + + it('should default to header based session w/ bad auth-mode header', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN' })], + }) + + const app = getTestApp() + + const resp = await createSession(app, 'lol') + assert.strictEqual(resp.accessToken, undefined) + assert.strictEqual(resp.refreshToken, undefined) + assert.strictEqual(resp.antiCsrf, undefined) + assert.notStrictEqual(resp.accessTokenFromHeader, undefined) + assert.notStrictEqual(resp.refreshTokenFromHeader, undefined) + }) + + it('should use headers if auth-mode specifies it', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN' })], + }) + + const app = getTestApp() + + const resp = await createSession(app, 'header') + assert.strictEqual(resp.accessToken, undefined) + assert.strictEqual(resp.refreshToken, undefined) + assert.strictEqual(resp.antiCsrf, undefined) + assert.notStrictEqual(resp.accessTokenFromHeader, undefined) + assert.notStrictEqual(resp.refreshTokenFromHeader, undefined) + }) + + it('should use cookies if auth-mode specifies it', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN' })], + }) + + const app = getTestApp() + + const resp = await createSession(app, 'cookie') + assert.notStrictEqual(resp.accessToken, undefined) + assert.notStrictEqual(resp.refreshToken, undefined) + assert.notStrictEqual(resp.antiCsrf, undefined) + assert.strictEqual(resp.accessTokenFromHeader, undefined) + assert.strictEqual(resp.refreshTokenFromHeader, undefined) + + // We check that we will have access token at least as long as we have a refresh token + // so verify session can return TRY_REFRESH_TOKEN + assert(Date.parse(resp.accessTokenExpiry) >= Date.parse(resp.refreshTokenExpiry)) + }) + }) + + describe('with user provided getTokenTransferMethod', () => { + it('should use headers if getTokenTransferMethod returns any', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN', getTokenTransferMethod: () => 'any' })], + }) + + const app = getTestApp() + + const resp = await createSession(app, 'cookie') + assert.strictEqual(resp.accessToken, undefined) + assert.strictEqual(resp.refreshToken, undefined) + assert.strictEqual(resp.antiCsrf, undefined) + assert.notStrictEqual(resp.accessTokenFromHeader, undefined) + assert.notStrictEqual(resp.refreshTokenFromHeader, undefined) + }) + + it('should use headers if getTokenTransferMethod returns header', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN', getTokenTransferMethod: () => 'header' })], + }) + + const app = getTestApp() + + const resp = await createSession(app, 'cookie') + assert.strictEqual(resp.accessToken, undefined) + assert.strictEqual(resp.refreshToken, undefined) + assert.strictEqual(resp.antiCsrf, undefined) + assert.notStrictEqual(resp.accessTokenFromHeader, undefined) + assert.notStrictEqual(resp.refreshTokenFromHeader, undefined) + }) + + it('should use clear cookies (if present) if getTokenTransferMethod returns header', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN', getTokenTransferMethod: () => 'header' })], + }) + + const app = getTestApp() + + const resp = extractInfoFromResponse( + await new Promise((resolve, reject) => { + const req = request(app).post('/create') + req.set('st-auth-mode', 'header') + return req + .set('Cookie', [`sAccessToken=${exampleJWT}`]) + .send(undefined) + .expect(200) + .end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }), + ) + assert.strictEqual(resp.accessToken, '') + assert.strictEqual(resp.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(resp.refreshToken, '') + assert.strictEqual(resp.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(resp.antiCsrf, undefined) + }) + + it('should use cookies if getTokenTransferMethod returns cookie', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN', getTokenTransferMethod: () => 'cookie' })], + }) + + const app = getTestApp() + + const resp = await createSession(app, 'header') + assert.notStrictEqual(resp.accessToken, undefined) + assert.notStrictEqual(resp.refreshToken, undefined) + assert.notStrictEqual(resp.antiCsrf, undefined) + assert.strictEqual(resp.accessTokenFromHeader, undefined) + assert.strictEqual(resp.refreshTokenFromHeader, undefined) + + // We check that we will have access token at least as long as we have a refresh token + // so verify session can return TRY_REFRESH_TOKEN + assert(Date.parse(resp.accessTokenExpiry) >= Date.parse(resp.refreshTokenExpiry)) + }) + + it('should clear headers (if present) if getTokenTransferMethod returns cookie', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN', getTokenTransferMethod: () => 'cookie' })], + }) + + const app = getTestApp() + + const resp = extractInfoFromResponse( + await new Promise((resolve, reject) => { + const req = request(app).post('/create') + req.set('st-auth-mode', 'header') + return req + .set('Authorization', `Bearer ${exampleJWT}`) + .send(undefined) + .expect(200) + .end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }), + ) + + assert.strictEqual(resp.accessTokenFromHeader, '') + assert.strictEqual(resp.refreshTokenFromHeader, '') + }) + }) + }) + + describe('verifySession', () => { + describe('from behaviour table', () => { + // prettier-ignore + const behaviourTable = [ + { getTokenTransferMethodRes: 'any', sessionRequired: false, authHeader: false, authCookie: false, output: 'undefined' }, + { getTokenTransferMethodRes: 'header', sessionRequired: false, authHeader: false, authCookie: false, output: 'undefined' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: false, authHeader: false, authCookie: false, output: 'undefined' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: false, authHeader: true, authCookie: false, output: 'undefined' }, + { getTokenTransferMethodRes: 'header', sessionRequired: false, authHeader: false, authCookie: true, output: 'undefined' }, + { getTokenTransferMethodRes: 'any', sessionRequired: true, authHeader: false, authCookie: false, output: 'UNAUTHORISED' }, + { getTokenTransferMethodRes: 'header', sessionRequired: true, authHeader: false, authCookie: false, output: 'UNAUTHORISED' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: true, authHeader: false, authCookie: false, output: 'UNAUTHORISED' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: true, authHeader: true, authCookie: false, output: 'UNAUTHORISED' }, + { getTokenTransferMethodRes: 'header', sessionRequired: true, authHeader: false, authCookie: true, output: 'UNAUTHORISED' }, + { getTokenTransferMethodRes: 'any', sessionRequired: true, authHeader: true, authCookie: true, output: 'validateheader' }, + { getTokenTransferMethodRes: 'any', sessionRequired: false, authHeader: true, authCookie: true, output: 'validateheader' }, + { getTokenTransferMethodRes: 'header', sessionRequired: true, authHeader: true, authCookie: true, output: 'validateheader' }, + { getTokenTransferMethodRes: 'header', sessionRequired: false, authHeader: true, authCookie: true, output: 'validateheader' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: true, authHeader: true, authCookie: true, output: 'validatecookie' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: false, authHeader: true, authCookie: true, output: 'validatecookie' }, + { getTokenTransferMethodRes: 'any', sessionRequired: true, authHeader: true, authCookie: false, output: 'validateheader' }, + { getTokenTransferMethodRes: 'any', sessionRequired: false, authHeader: true, authCookie: false, output: 'validateheader' }, + { getTokenTransferMethodRes: 'header', sessionRequired: true, authHeader: true, authCookie: false, output: 'validateheader' }, + { getTokenTransferMethodRes: 'header', sessionRequired: false, authHeader: true, authCookie: false, output: 'validateheader' }, + { getTokenTransferMethodRes: 'any', sessionRequired: true, authHeader: false, authCookie: true, output: 'validatecookie' }, + { getTokenTransferMethodRes: 'any', sessionRequired: false, authHeader: false, authCookie: true, output: 'validatecookie' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: true, authHeader: false, authCookie: true, output: 'validatecookie' }, + { getTokenTransferMethodRes: 'cookie', sessionRequired: false, authHeader: false, authCookie: true, output: 'validatecookie' }, + ] + + for (let i = 0; i < behaviourTable.length; ++i) { + const conf = behaviourTable[i] + it(`should match line ${i + 1} with a valid token`, async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => conf.getTokenTransferMethodRes, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfo = await createSession(app, 'cookie') + + const authMode + = conf.authCookie && conf.authHeader + ? 'both' + : conf.authCookie + ? 'cookie' + : conf.authHeader + ? 'header' + : 'none' + + const res = await testGet( + app, + createInfo, + conf.sessionRequired ? '/verify' : '/verify-optional', + conf.output === 'UNAUTHORISED' ? 401 : 200, + authMode, + ) + switch (conf.output) { + case 'undefined': + assert.strictEqual(res.body.sessionExists, false) + break + case 'UNAUTHORISED': + assert.deepStrictEqual(res.body, { message: 'unauthorised' }) + break + case 'validatecookie': + case 'validateheader': + assert.strictEqual(res.body.sessionExists, true) + } + }) + + it(`should match line ${i + 1} with a expired token`, async () => { + await setKeyValueInConfig('access_token_validity', 2) + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => conf.getTokenTransferMethodRes, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfo = await createSession(app, 'cookie') + await delay(3) + const authMode + = conf.authCookie && conf.authHeader + ? 'both' + : conf.authCookie + ? 'cookie' + : conf.authHeader + ? 'header' + : 'none' + + const res = await testGet( + app, + createInfo, + conf.sessionRequired ? '/verify' : '/verify-optional', + conf.output !== 'undefined' ? 401 : 200, + authMode, + ) + switch (conf.output) { + case 'undefined': + assert.strictEqual(res.body.sessionExists, false) + break + case 'UNAUTHORISED': + assert.deepStrictEqual(res.body, { message: 'unauthorised' }) + break + case 'validatecookie': + case 'validateheader': + assert.deepStrictEqual(res.body, { message: 'try refresh token' }) + } + }) + } + }) + + describe('with access tokens in both headers and cookies', () => { + it('should use the value from headers if getTokenTransferMethod returns any', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'any', + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfoCookie = await createSession(app, 'header') + const createInfoHeader = await createSession(app, 'header') + + const resp = await new Promise((resolve, reject) => { + const req = request(app).get('/verify') + + req.set('Cookie', [`sAccessToken=${createInfoCookie.accessTokenFromHeader}`]) + if (createInfoCookie.antiCsrf) + req.set('anti-csrf', info.antiCsrf) + + req.set( + 'Authorization', + `Bearer ${decodeURIComponent(createInfoHeader.accessTokenFromHeader)}`, + ) + req.expect(200).end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }) + + assert.strictEqual(resp.body.sessionHandle, createInfoHeader.body.sessionHandle) + }) + + it('should use the value from headers if getTokenTransferMethod returns header', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: ({ forCreateNewSession }) => + forCreateNewSession ? 'any' : 'header', + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfoCookie = await createSession(app, 'header') + const createInfoHeader = await createSession(app, 'header') + + const resp = await new Promise((resolve, reject) => { + const req = request(app).get('/verify') + + req.set('Cookie', [`sAccessToken=${createInfoCookie.accessTokenFromHeader}`]) + if (createInfoCookie.antiCsrf) + req.set('anti-csrf', info.antiCsrf) + + req.set( + 'Authorization', + `Bearer ${decodeURIComponent(createInfoHeader.accessTokenFromHeader)}`, + ) + req.expect(200).end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }) + + assert.strictEqual(resp.body.sessionHandle, createInfoHeader.body.sessionHandle) + }) + + it('should use the value from cookies if getTokenTransferMethod returns cookie', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: ({ forCreateNewSession }) => + forCreateNewSession ? 'any' : 'cookie', + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfoCookie = await createSession(app, 'header') + const createInfoHeader = await createSession(app, 'header') + + const resp = await new Promise((resolve, reject) => { + const req = request(app).get('/verify') + + req.set('Cookie', [`sAccessToken=${createInfoCookie.accessTokenFromHeader}`]) + if (createInfoCookie.antiCsrf) + req.set('anti-csrf', info.antiCsrf) + + req.set( + 'Authorization', + `Bearer ${decodeURIComponent(createInfoHeader.accessTokenFromHeader)}`, + ) + req.expect(200).end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }) + + assert.strictEqual(resp.body.sessionHandle, createInfoCookie.body.sessionHandle) + }) + }) + + it('should reject requests with sIdRefreshToken', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfo = await createSession(app, 'cookie') + + const res = await new Promise((resolve, reject) => + request(app) + .get('/verify') + .set('Cookie', [ + `sAccessToken=${createInfo.accessToken}`, + `sIdRefreshToken=${createInfo.refreshToken}`, // The value doesn't actually matter + ]) + .set('anti-csrf', createInfo.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(res.status, 401) + assert.deepStrictEqual(res.body, { message: 'try refresh token' }) + }) + + describe('with non ST in Authorize header', () => { + it('should use the value from cookies if present and getTokenTransferMethod returns any', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'any', + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfoCookie = await createSession(app, 'header') + + const resp = await new Promise((resolve, reject) => { + const req = request(app).get('/verify') + + req.set('Cookie', [`sAccessToken=${createInfoCookie.accessTokenFromHeader}`]) + if (createInfoCookie.antiCsrf) + req.set('anti-csrf', info.antiCsrf) + + req.set('Authorization', `Bearer ${exampleJWT}`) + req.expect(200).end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }) + + assert.strictEqual(resp.body.sessionHandle, createInfoCookie.body.sessionHandle) + }) + + it('should reject with UNAUTHORISED if getTokenTransferMethod returns header', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: ({ forCreateNewSession }) => + forCreateNewSession ? 'any' : 'header', + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const createInfoCookie = await createSession(app, 'header') + + const resp = await new Promise((resolve, reject) => { + const req = request(app).get('/verify') + + req.set('Cookie', [`sAccessToken=${createInfoCookie.accessTokenFromHeader}`]) + if (createInfoCookie.antiCsrf) + req.set('anti-csrf', info.antiCsrf) + + req.set('Authorization', `Bearer ${exampleJWT}`) + req.end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }) + assert.strictEqual(resp.status, 401) + assert.deepStrictEqual(resp.body, { message: 'unauthorised' }) + }) + + it('should reject with UNAUTHORISED if cookies are not present', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: ({ }) => 'any', + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const resp = await new Promise((resolve, reject) => { + const req = request(app).get('/verify') + + req.set('Authorization', `Bearer ${exampleJWT}`) + req.end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }) + }) + + assert.strictEqual(resp.status, 401) + assert.deepStrictEqual(resp.body, { message: 'unauthorised' }) + }) + }) + }) + + return + describe('mergeIntoAccessTokenPayload', () => { + it('should update cookies if the session was cookie based', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN' })], + }) + + const app = getTestApp() + + const createInfo = await createSession(app, 'header') + + const updateInfo = extractInfoFromResponse( + await testGet(app, createInfo, '/update-payload', 200, 'cookie', undefined), + ) + + // Didn't update + assert.strictEqual(updateInfo.refreshToken, undefined) + assert.strictEqual(updateInfo.antiCsrf, undefined) + assert.strictEqual(updateInfo.accessTokenFromHeader, undefined) + assert.strictEqual(updateInfo.refreshTokenFromHeader, undefined) + + // Updated access token + assert.notStrictEqual(updateInfo.accessToken, undefined) + assert.notStrictEqual(updateInfo.accessToken, createInfo.accessTokenFromHeader) + + // Updated front token + assert.notStrictEqual(updateInfo.frontToken, undefined) + assert.notStrictEqual(updateInfo.frontToken, createInfo.frontToken) + }) + + it('should allow headers if the session was header based', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ antiCsrf: 'VIA_TOKEN' })], + }) + + const app = getTestApp() + + const createInfo = await createSession(app, 'cookie') + + const updateInfo = extractInfoFromResponse( + await testGet(app, createInfo, '/update-payload', 200, 'header', undefined), + ) + + // Didn't update + assert.strictEqual(updateInfo.accessToken, undefined) + assert.strictEqual(updateInfo.refreshToken, undefined) + assert.strictEqual(updateInfo.antiCsrf, undefined) + assert.strictEqual(updateInfo.refreshTokenFromHeader, undefined) + + // Updated access token + assert.notStrictEqual(updateInfo.accessTokenFromHeader, undefined) + assert.notStrictEqual(updateInfo.accessTokenFromHeader, createInfo.accessToken) + + // Updated front token + assert.notStrictEqual(updateInfo.frontToken, undefined) + assert.notStrictEqual(updateInfo.frontToken, createInfo.frontToken) + }) + }) + + describe('refreshSession', () => { + describe('from behaviour table', () => { + // prettier-ignore + const behaviourTable = [ + { getTokenTransferMethodRes: 'any', authHeader: false, authCookie: false, output: 'unauthorised', setTokens: 'none', clearedTokens: 'none' }, + { getTokenTransferMethodRes: 'header', authHeader: false, authCookie: false, output: 'unauthorised', setTokens: 'none', clearedTokens: 'none' }, + { getTokenTransferMethodRes: 'cookie', authHeader: false, authCookie: false, output: 'unauthorised', setTokens: 'none', clearedTokens: 'none' }, + { getTokenTransferMethodRes: 'any', authHeader: false, authCookie: true, output: 'validatecookie', setTokens: 'cookies', clearedTokens: 'none' }, + { getTokenTransferMethodRes: 'header', authHeader: false, authCookie: true, output: 'unauthorised', setTokens: 'none', clearedTokens: 'none' }, // 5 + { getTokenTransferMethodRes: 'cookie', authHeader: false, authCookie: true, output: 'validatecookie', setTokens: 'cookies', clearedTokens: 'none' }, + { getTokenTransferMethodRes: 'any', authHeader: true, authCookie: false, output: 'validateheader', setTokens: 'headers', clearedTokens: 'none' }, + { getTokenTransferMethodRes: 'header', authHeader: true, authCookie: false, output: 'validateheader', setTokens: 'headers', clearedTokens: 'none' }, + { getTokenTransferMethodRes: 'cookie', authHeader: true, authCookie: false, output: 'unauthorised', setTokens: 'none', clearedTokens: 'none' }, // 9 + { getTokenTransferMethodRes: 'any', authHeader: true, authCookie: true, output: 'validateheader', setTokens: 'headers', clearedTokens: 'cookies' }, + { getTokenTransferMethodRes: 'header', authHeader: true, authCookie: true, output: 'validateheader', setTokens: 'headers', clearedTokens: 'cookies' }, + { getTokenTransferMethodRes: 'cookie', authHeader: true, authCookie: true, output: 'validatecookie', setTokens: 'cookies', clearedTokens: 'headers' }, // 12 + ] + + for (let i = 0; i < behaviourTable.length; ++i) { + const conf = behaviourTable[i] + it(`should match line ${i + 1} with a valid token`, async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => conf.getTokenTransferMethodRes, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + // Which we create doesn't really matter, since the token is the same + const createInfo = await createSession(app, 'header') + + const authMode + = conf.authCookie && conf.authHeader + ? 'both' + : conf.authCookie + ? 'cookie' + : conf.authHeader + ? 'header' + : 'none' + + const refreshRes = await refreshSession( + app, + conf.getTokenTransferMethodRes, + authMode, + createInfo, + ) + + if (conf.output === 'unauthorised') { + assert.strictEqual(refreshRes.status, 401) + assert.deepStrictEqual(refreshRes.body, { message: 'unauthorised' }) + } + else { + assert.strictEqual(refreshRes.status, 200) + } + + if (conf.clearedTokens === 'headers') { + assert.strictEqual(refreshRes.accessTokenFromHeader, '') + assert.strictEqual(refreshRes.refreshTokenFromHeader, '') + } + else if (conf.clearedTokens === 'cookies') { + assert.strictEqual(refreshRes.accessToken, '') + assert.strictEqual(refreshRes.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(refreshRes.refreshToken, '') + assert.strictEqual(refreshRes.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + } + + switch (conf.setTokens) { + case 'headers': + assert.ok(refreshRes.accessTokenFromHeader) + assert.notStrictEqual(refreshRes.accessTokenFromHeader, '') + assert.ok(refreshRes.refreshTokenFromHeader) + assert.notStrictEqual(refreshRes.refreshTokenFromHeader, '') + break + case 'cookies': + assert.notStrictEqual(refreshRes.accessToken, '') + assert.notStrictEqual(refreshRes.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.notStrictEqual(refreshRes.refreshToken, '') + assert.notStrictEqual(refreshRes.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + break + case 'none': + if (conf.clearedTokens === 'none') + assert.strictEqual(refreshRes.frontToken, undefined) + + break + } + if (conf.setTokens !== 'cookies' && conf.clearedTokens !== 'cookies') { + assert.strictEqual(refreshRes.accessToken, undefined) + assert.strictEqual(refreshRes.accessTokenExpiry, undefined) + assert.strictEqual(refreshRes.refreshToken, undefined) + assert.strictEqual(refreshRes.refreshTokenExpiry, undefined) + } + if (conf.setTokens !== 'headers' && conf.clearedTokens !== 'headers') { + assert.strictEqual(refreshRes.accessTokenFromHeader, undefined) + assert.strictEqual(refreshRes.refreshTokenFromHeader, undefined) + } + }) + } + + for (let i = 0; i < behaviourTable.length; ++i) { + const conf = behaviourTable[i] + + it(`should match line ${i + 1} with a invalid token`, async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => conf.getTokenTransferMethodRes, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const infoWithInvalidRefreshToken = { + refreshToken: 'asdf', + } + + const authMode + = conf.authCookie && conf.authHeader + ? 'both' + : conf.authCookie + ? 'cookie' + : conf.authHeader + ? 'header' + : 'none' + + const refreshRes = await refreshSession( + app, + conf.getTokenTransferMethodRes, + authMode, + infoWithInvalidRefreshToken, + ) + + assert.strictEqual(refreshRes.status, 401) + assert.deepStrictEqual(refreshRes.body, { message: 'unauthorised' }) + if (conf.output === 'validateheader') { + assert.strictEqual(refreshRes.accessTokenFromHeader, '') + assert.strictEqual(refreshRes.refreshTokenFromHeader, '') + } + else if (conf.output === 'validatecookie') { + assert.strictEqual(refreshRes.accessToken, '') + assert.strictEqual(refreshRes.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(refreshRes.refreshToken, '') + assert.strictEqual(refreshRes.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + } + }) + } + }) + }) + }) +}) diff --git a/test/config.test.js b/test/config.test.js deleted file mode 100644 index 161ca2045..000000000 --- a/test/config.test.js +++ /dev/null @@ -1,1593 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - resetAll, - extractInfoFromResponse, - mockResponse, - mockRequest, -} = require("./utils"); -const request = require("supertest"); -const express = require("express"); -let STExpress = require("../"); -let Session = require("../recipe/session"); -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); -let NormalisedURLPath = require("../lib/build/normalisedURLPath").default; -let NormalisedURLDomain = require("../lib/build/normalisedURLDomain").default; -let { normaliseSessionScopeOrThrowError } = require("../lib/build/recipe/session/utils"); -const { Querier } = require("../lib/build/querier"); -let SuperTokens = require("../lib/build/supertokens").default; -let ST = require("../"); -let EmailPassword = require("../lib/build/recipe/emailpassword"); -let EmailPasswordRecipe = require("../lib/build/recipe/emailpassword/recipe").default; -const { getTopLevelDomainForSameSiteResolution } = require("../lib/build/utils"); -const { middleware } = require("../framework/express"); - -describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // test various inputs for appInfo - // Failure condition: passing data of invalid type/ syntax to appInfo - it("test values for optional inputs for appInfo", async function () { - await startST(); - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === "/auth"); - assert(SuperTokens.getInstanceOrThrowError().appInfo.websiteBasePath.getAsStringDangerous() === "/auth"); - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === "/test"); - assert(SuperTokens.getInstanceOrThrowError().appInfo.websiteBasePath.getAsStringDangerous() === "/test1"); - - resetAll(); - } - }); - - it("test values for compulsory inputs for appInfo", async function () { - await startST(); - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(false); - } catch (err) { - if ( - err.message !== - "Please provide your apiDomain inside the appInfo object when calling supertokens.init" - ) { - throw err; - } - } - - resetAll(); - } - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(false); - } catch (err) { - if ( - err.message !== - "Please provide your appName inside the appInfo object when calling supertokens.init" - ) { - throw err; - } - } - - resetAll(); - } - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(false); - } catch (err) { - if ( - err.message !== - "Please provide your websiteDomain inside the appInfo object when calling supertokens.init" - ) { - throw err; - } - } - - resetAll(); - } - }); - - // test using zero, one and two recipe modules - // Failure condition: initial supertokens with the incorrect number of modules as specified in the checks - it("test using zero, one and two recipe modules", async function () { - await startST(); - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [], - }); - assert(false); - } catch (err) { - if (err.message !== "Please provide at least one recipe to the supertokens.init function call") { - throw err; - } - } - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - SessionRecipe.getInstanceOrThrowError(); - assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 1); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" }), EmailPassword.init()], - }); - SessionRecipe.getInstanceOrThrowError(); - EmailPasswordRecipe.getInstanceOrThrowError(); - assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 2); - resetAll(); - } - }); - - // test config for session module - // Failure condition: passing data of invalid type/ syntax to the modules config - it("test config for session module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - cookieDomain: "testDomain", - sessionExpiredStatusCode: 111, - cookieSecure: true, - }), - ], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieDomain === "testdomain"); - assert(SessionRecipe.getInstanceOrThrowError().config.sessionExpiredStatusCode === 111); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - }); - - it("various sameSite values", async function () { - await startST(); - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: " Lax " })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "None " })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "none"); - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: " STRICT " })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "strict"); - - resetAll(); - } - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "random " })], - }); - assert(false); - } catch (err) { - if (err.message !== 'cookie same site must be one of "strict", "lax", or "none"') { - throw error; - } - } - - resetAll(); - } - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: " " })], - }); - assert(false); - } catch (err) { - if (err.message !== 'cookie same site must be one of "strict", "lax", or "none"') { - throw error; - } - } - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "lax" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "none" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "none"); - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "strict" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "strict"); - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - - resetAll(); - } - }); - - it("sameSite none invalid domain values", async function () { - const domainCombinations = [ - ["http://localhost:3000", "http://supertokensapi.io"], - ["http://127.0.0.1:3000", "http://supertokensapi.io"], - ["http://supertokens.io", "http://localhost:8000"], - ["http://supertokens.io", "http://127.0.0.1:8000"], - ["http://supertokens.io", "http://supertokensapi.io"], - ]; - - for (const domainCombination of domainCombinations) { - let err; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - appName: "SuperTokens", - websiteDomain: domainCombination[0], - apiDomain: domainCombination[1], - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "none" })], - }); - try { - await Session.createNewSession(mockRequest(), mockResponse(), "asdf"); - } catch (e) { - err = e; - } - assert.ok(err); - assert.strictEqual( - err.message, - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." - ); - resetAll(); - } - }); - - it("sameSite none valid domain values", async function () { - const domainCombinations = [ - ["http://localhost:3000", "http://localhost:8000"], - ["http://127.0.0.1:3000", "http://localhost:8000"], - ["http://localhost:3000", "http://127.0.0.1:8000"], - ["http://127.0.0.1:3000", "http://127.0.0.1:8000"], - - ["https://localhost:3000", "https://localhost:8000"], - ["https://127.0.0.1:3000", "https://localhost:8000"], - ["https://localhost:3000", "https://127.0.0.1:8000"], - ["https://127.0.0.1:3000", "https://127.0.0.1:8000"], - - ["https://supertokens.io", "https://api.supertokens.io"], - ["https://supertokens.io", "https://supertokensapi.io"], - - ["http://localhost:3000", "https://supertokensapi.io"], - ["http://127.0.0.1:3000", "https://supertokensapi.io"], - ]; - - for (const domainCombination of domainCombinations) { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - appName: "SuperTokens", - websiteDomain: domainCombination[0], - apiDomain: domainCombination[1], - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "none" })], - }); - } catch (e) { - assert(false); - } - resetAll(); - } - }); - - it("testing sessionScope normalisation", async function () { - assert(normaliseSessionScopeOrThrowError("api.example.com") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("http://api.example.com") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("https://api.example.com") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("http://api.example.com?hello=1") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("http://api.example.com/hello") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("http://api.example.com/") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("http://api.example.com:8080") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("http://api.example.com#random2") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("api.example.com/") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("api.example.com#random") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("example.com") === "example.com"); - assert(normaliseSessionScopeOrThrowError("api.example.com/?hello=1&bye=2") === "api.example.com"); - assert(normaliseSessionScopeOrThrowError("localhost.org") === "localhost.org"); - assert(normaliseSessionScopeOrThrowError("localhost") === "localhost"); - assert(normaliseSessionScopeOrThrowError("localhost:8080") === "localhost"); - assert(normaliseSessionScopeOrThrowError("localhost.org") === "localhost.org"); - assert(normaliseSessionScopeOrThrowError("127.0.0.1") === "127.0.0.1"); - - assert(normaliseSessionScopeOrThrowError(".api.example.com") === ".api.example.com"); - assert(normaliseSessionScopeOrThrowError(".api.example.com/") === ".api.example.com"); - assert(normaliseSessionScopeOrThrowError(".api.example.com#random") === ".api.example.com"); - assert(normaliseSessionScopeOrThrowError(".example.com") === ".example.com"); - assert(normaliseSessionScopeOrThrowError(".api.example.com/?hello=1&bye=2") === ".api.example.com"); - assert(normaliseSessionScopeOrThrowError(".localhost.org") === ".localhost.org"); - assert(normaliseSessionScopeOrThrowError(".localhost") === "localhost"); - assert(normaliseSessionScopeOrThrowError(".localhost:8080") === "localhost"); - assert(normaliseSessionScopeOrThrowError(".localhost.org") === ".localhost.org"); - assert(normaliseSessionScopeOrThrowError(".127.0.0.1") === "127.0.0.1"); - - try { - normaliseSessionScopeOrThrowError("http://"); - assert(false); - } catch (err) { - assert(err.message === "Please provide a valid sessionScope"); - } - }); - - it("testing URL path normalisation", async function () { - function normaliseURLPathOrThrowError(input) { - return new NormalisedURLPath(input).getAsStringDangerous(); - } - - assert.strictEqual(normaliseURLPathOrThrowError("exists?email=john.doe%40gmail.com"), "/exists"); - assert.strictEqual( - normaliseURLPathOrThrowError("/auth/email/exists?email=john.doe%40gmail.com"), - "/auth/email/exists" - ); - assert.strictEqual(normaliseURLPathOrThrowError("exists"), "/exists"); - assert.strictEqual(normaliseURLPathOrThrowError("/exists"), "/exists"); - assert.strictEqual(normaliseURLPathOrThrowError("/exists?email=john.doe%40gmail.com"), "/exists"); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("https://api.example.com"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com?hello=1"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com/hello"), "/hello"); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com/"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com:8080"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com#random2"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("api.example.com/"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("api.example.com#random"), ""); - assert.strictEqual(normaliseURLPathOrThrowError(".example.com"), ""); - assert.strictEqual(normaliseURLPathOrThrowError("api.example.com/?hello=1&bye=2"), ""); - - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("http://1.2.3.4/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("1.2.3.4/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("https://api.example.com/one/two/"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com/one/two?hello=1"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com/hello/"), "/hello"); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com/one/two/"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com:8080/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("http://api.example.com/one/two#random2"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("api.example.com/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("api.example.com/one/two/#random"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError(".example.com/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("api.example.com/one/two?hello=1&bye=2"), "/one/two"); - - assert.strictEqual(normaliseURLPathOrThrowError("/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("one/two/"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("/one"), "/one"); - assert.strictEqual(normaliseURLPathOrThrowError("one"), "/one"); - assert.strictEqual(normaliseURLPathOrThrowError("one/"), "/one"); - assert.strictEqual(normaliseURLPathOrThrowError("/one/two/"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("/one/two?hello=1"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("one/two?hello=1"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("/one/two/#random"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("one/two#random"), "/one/two"); - - assert.strictEqual(normaliseURLPathOrThrowError("localhost:4000/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("127.0.0.1:4000/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("127.0.0.1/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("https://127.0.0.1:80/one/two"), "/one/two"); - assert.strictEqual(normaliseURLPathOrThrowError("/"), ""); - assert.strictEqual(normaliseURLPathOrThrowError(""), ""); - - assert.strictEqual(normaliseURLPathOrThrowError("/.netlify/functions/api"), "/.netlify/functions/api"); - assert.strictEqual(normaliseURLPathOrThrowError("/netlify/.functions/api"), "/netlify/.functions/api"); - assert.strictEqual( - normaliseURLPathOrThrowError("app.example.com/.netlify/functions/api"), - "/.netlify/functions/api" - ); - assert.strictEqual( - normaliseURLPathOrThrowError("app.example.com/netlify/.functions/api"), - "/netlify/.functions/api" - ); - assert.strictEqual(normaliseURLPathOrThrowError("/app.example.com"), "/app.example.com"); - }); - - it("testing URL domain normalisation", async function () { - function normaliseURLDomainOrThrowError(input) { - return new NormalisedURLDomain(input).getAsStringDangerous(); - } - assert(normaliseURLDomainOrThrowError("http://api.example.com") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("https://api.example.com") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError("http://api.example.com?hello=1") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("http://api.example.com/hello") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("http://api.example.com/") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("http://api.example.com:8080") === "http://api.example.com:8080"); - assert(normaliseURLDomainOrThrowError("http://api.example.com#random2") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("api.example.com/") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError("api.example.com") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError("api.example.com#random") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError(".example.com") === "https://example.com"); - assert(normaliseURLDomainOrThrowError("api.example.com/?hello=1&bye=2") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError("localhost") === "http://localhost"); - assert(normaliseURLDomainOrThrowError("https://localhost") === "https://localhost"); - - assert(normaliseURLDomainOrThrowError("http://api.example.com/one/two") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("http://1.2.3.4/one/two") === "http://1.2.3.4"); - assert(normaliseURLDomainOrThrowError("https://1.2.3.4/one/two") === "https://1.2.3.4"); - assert(normaliseURLDomainOrThrowError("1.2.3.4/one/two") === "http://1.2.3.4"); - assert(normaliseURLDomainOrThrowError("https://api.example.com/one/two/") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError("http://api.example.com/one/two?hello=1") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("http://api.example.com/one/two#random2") === "http://api.example.com"); - assert(normaliseURLDomainOrThrowError("api.example.com/one/two") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError("api.example.com/one/two/#random") === "https://api.example.com"); - assert(normaliseURLDomainOrThrowError(".example.com/one/two") === "https://example.com"); - assert(normaliseURLDomainOrThrowError("localhost:4000") === "http://localhost:4000"); - assert(normaliseURLDomainOrThrowError("127.0.0.1:4000") === "http://127.0.0.1:4000"); - assert(normaliseURLDomainOrThrowError("127.0.0.1") === "http://127.0.0.1"); - assert(normaliseURLDomainOrThrowError("https://127.0.0.1:80/") === "https://127.0.0.1:80"); - - try { - normaliseURLDomainOrThrowError("/one/two"); - assert(false); - } catch (err) { - assert(err.message === "Please provide a valid domain name"); - } - - try { - normaliseURLDomainOrThrowError("/.netlify/functions/api"); - assert(false); - } catch (err) { - assert(err.message === "Please provide a valid domain name"); - } - }); - - it("various config values", async function () { - await startST(); - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", accessTokenPath: "/access" })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.accessTokenPath.getAsStringDangerous() === "/access"); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.accessTokenPath.getAsStringDangerous() === ""); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/custom/a", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert( - SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous() === - "/custom/a/session/refresh" - ); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert( - SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous() === - "/session/refresh" - ); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert( - SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous() === - "/auth/session/refresh" - ); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - apiKey: "haha", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(Querier.apiKey === "haha"); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/custom", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", sessionExpiredStatusCode: 402 })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.sessionExpiredStatusCode === 402); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080;try.supertokens.io;try.supertokens.io:8080;localhost:90", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - let hosts = Querier.hosts; - assert(hosts.length === 4); - - assert(hosts[0].domain.getAsStringDangerous() === "http://localhost:8080"); - assert(hosts[1].domain.getAsStringDangerous() === "https://try.supertokens.io"); - assert(hosts[2].domain.getAsStringDangerous() === "https://try.supertokens.io:8080"); - assert(hosts[3].domain.getAsStringDangerous() === "http://localhost:90"); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_CUSTOM_HEADER" })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "VIA_CUSTOM_HEADER"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "https://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "NONE"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "https://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "NONE"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "NONE"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.com", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "VIA_CUSTOM_HEADER"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "none"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.co.uk", - appName: "SuperTokens", - websiteDomain: "supertokens.co.uk", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "NONE"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "127.0.0.1:3000", - appName: "SuperTokens", - websiteDomain: "127.0.0.1:9000", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "NONE"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === false); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "127.0.0.1:3000", - appName: "SuperTokens", - websiteDomain: "127.0.0.1:9000", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_CUSTOM_HEADER" })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "VIA_CUSTOM_HEADER"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === false); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "127.0.0.1:9000", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "VIA_CUSTOM_HEADER"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "none"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "127.0.0.1:3000", - appName: "SuperTokens", - websiteDomain: "127.0.0.1:9000", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "NONE"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "lax"); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === false); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "https://127.0.0.1:3000", - appName: "SuperTokens", - websiteDomain: "google.com", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "NONE" })], - }); - assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === "NONE"); - resetAll(); - } - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "127.0.0.1:3000", - appName: "SuperTokens", - websiteDomain: "google.com", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "RANDOM" })], - }); - assert(false); - } catch (err) { - if (err.message !== "antiCsrf config must be one of 'NONE' or 'VIA_CUSTOM_HEADER' or 'VIA_TOKEN'") { - throw err; - } - } - resetAll(); - } - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.test.com:3000", - appName: "SuperTokens", - websiteDomain: "google.com", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - assert(false); - } catch (err) { - assert.strictEqual( - err.message, - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." - ); - } - resetAll(); - } - - { - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "https://api.test.com:3000", - appName: "SuperTokens", - websiteDomain: "google.com", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", cookieSecure: false })], - }); - await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - assert(false); - } catch (err) { - assert.strictEqual( - err.message, - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." - ); - } - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "https://api.test.com:3000", - appName: "SuperTokens", - websiteDomain: "google.com", - apiBasePath: "test/", - websiteBasePath: "test1/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - getTokenTransferMethod: () => "header", - cookieSecure: false, - }), - ], - }); - await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "https://localhost", - appName: "Supertokens", - websiteDomain: "http://localhost:3000", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure); - assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === "none"); - - resetAll(); - } - }); - - it("checking for default cookie config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert.equal(SessionRecipe.getInstanceOrThrowError().config.cookieDomain, undefined); - assert.equal(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite, "lax"); - assert.equal(SessionRecipe.getInstanceOrThrowError().config.cookieSecure, true); - assert.equal( - SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous(), - "/auth/session/refresh" - ); - assert.equal(SessionRecipe.getInstanceOrThrowError().config.sessionExpiredStatusCode, 401); - }); - - it("Test that the jwt feature is disabled by default", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined); - assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.enable, false); - }); - - it("Test that the jwt feature is disabled when explicitly set to false", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", jwt: { enable: false } })], - }); - assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined); - assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.enable, false); - }); - - it("Test that the jwt feature is enabled when explicitly set to true", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", jwt: { enable: true } })], - }); - - assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined); - assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.enable, true); - }); - - it("Test that the custom jwt property name in access token payload is set correctly in config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true, propertyNameInAccessTokenPayload: "customJWTKey" }, - }), - ], - }); - - assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined); - assert.strictEqual( - SessionRecipe.getInstanceOrThrowError().config.jwt.propertyNameInAccessTokenPayload, - "customJWTKey" - ); - }); - - it("Test that the the jwt property name uses default value when not set in config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", jwt: { enable: true } })], - }); - - assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined); - assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.propertyNameInAccessTokenPayload, "jwt"); - }); - - it("Test that when setting jwt property name with the same value as the reserved property, init throws an error", async function () { - try { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true, propertyNameInAccessTokenPayload: "_jwtPName" }, - }), - ], - }); - - throw new Error("Init succeeded when it should have failed"); - } catch (e) { - if (e.message !== "_jwtPName is a reserved property name, please use a different key name for the jwt") { - throw e; - } - } - }); - - it("testing getTopLevelDomainForSameSiteResolution function", async function () { - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://a.b.test.com"), "test.com"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("https://a.b.test.com"), "test.com"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://a.b.test.co.uk"), "test.co.uk"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://a.b.test.co.uk"), "test.co.uk"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://test.com"), "test.com"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("https://test.com"), "test.com"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://localhost"), "localhost"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://localhost.org"), "localhost"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://8.8.8.8"), "localhost"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://8.8.8.8:8080"), "localhost"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://localhost:3000"), "localhost"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("http://test.com:3567"), "test.com"); - assert.strictEqual(getTopLevelDomainForSameSiteResolution("https://test.com:3567"), "test.com"); - }); - - it("apiGatewayPath test", async function () { - await startST(); - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiGatewayPath: "/gateway", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.status === 200); - - assert( - SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === "/gateway/auth" - ); - resetAll(); - } - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "hello", - apiGatewayPath: "/gateway", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/hello/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.status === 200); - - assert( - SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === "/gateway/hello" - ); - resetAll(); - } - - { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "hello", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/hello/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.status === 200); - - assert(SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === "/hello"); - resetAll(); - } - }); - - it("checking for empty item in recipeList config", async function () { - await startST(); - let errorCaught = true; - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" }), , EmailPassword.init()], - }); - errorCaught = false; - } catch (err) { - assert.strictEqual(err.message, "Please remove empty items from recipeList"); - } - assert(errorCaught); - }); - - it("Check that telemetry is set to true properly", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init()], - telemetry: true, - }); - - assert(SuperTokens.getInstanceOrThrowError().telemetryEnabled === true); - }); - - it("Check that telemetry is set to false by default", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init()], - }); - - assert(SuperTokens.getInstanceOrThrowError().telemetryEnabled === false); - }); - - it("Check that telemetry is set to false properly", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init()], - telemetry: false, - }); - - assert(SuperTokens.getInstanceOrThrowError().telemetryEnabled === false); - }); -}); diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 000000000..5af937fb2 --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,1607 @@ +/* eslint-disable no-lone-blocks */ +/* eslint no-lone-blocks: "error" */ +/* eslint-env es6 */ + +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import request from 'supertest' +import express from 'express' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { ProcessState } from 'supertokens-node/processState' +import NormalisedURLPath from 'supertokens-node/normalisedURLPath' +import NormalisedURLDomain from 'supertokens-node/normalisedURLDomain' +import { normaliseSessionScopeOrThrowError } from 'supertokens-node/recipe/session/utils' +import { Querier } from 'supertokens-node/querier' +import SuperTokens from 'supertokens-node/supertokens' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword/recipe' +import { getTopLevelDomainForSameSiteResolution } from 'supertokens-node/utils' +import { middleware } from 'supertokens-node/framework/express' +import { + cleanST, + extractInfoFromResponse, + killAllST, + mockRequest, + mockResponse, + printPath, + resetAll, + setupST, + startST, +} from './utils' + +describe(`configTest: ${printPath('[test/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // test various inputs for appInfo + // Failure condition: passing data of invalid type/ syntax to appInfo + it('test values for optional inputs for appInfo', async () => { + await startST() + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert(SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === '/auth') + assert(SuperTokens.getInstanceOrThrowError().appInfo.websiteBasePath.getAsStringDangerous() === '/auth') + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === '/test') + assert(SuperTokens.getInstanceOrThrowError().appInfo.websiteBasePath.getAsStringDangerous() === '/test1') + + resetAll() + } + }) + + it('test values for compulsory inputs for appInfo', async () => { + await startST() + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert(false) + } + catch (err) { + if ( + err.message + !== 'Please provide your apiDomain inside the appInfo object when calling supertokens.init' + ) + throw err + } + + resetAll() + } + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert(false) + } + catch (err) { + if ( + err.message + !== 'Please provide your appName inside the appInfo object when calling supertokens.init' + ) + throw err + } + + resetAll() + } + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert(false) + } + catch (err) { + if ( + err.message + !== 'Please provide your websiteDomain inside the appInfo object when calling supertokens.init' + ) + throw err + } + + resetAll() + } + }) + + // test using zero, one and two recipe modules + // Failure condition: initial supertokens with the incorrect number of modules as specified in the checks + it('test using zero, one and two recipe modules', async () => { + await startST() + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [], + }) + assert(false) + } + catch (err) { + if (err.message !== 'Please provide at least one recipe to the supertokens.init function call') + throw err + } + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + SessionRecipe.getInstanceOrThrowError() + assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 1) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' }), EmailPassword.init()], + }) + SessionRecipe.getInstanceOrThrowError() + EmailPasswordRecipe.getInstanceOrThrowError() + assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 2) + resetAll() + } + }) + + // test config for session module + // Failure condition: passing data of invalid type/ syntax to the modules config + it('test config for session module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + cookieDomain: 'testDomain', + sessionExpiredStatusCode: 111, + cookieSecure: true, + }), + ], + }) + assert(SessionRecipe.getInstanceOrThrowError().config.cookieDomain === 'testdomain') + assert(SessionRecipe.getInstanceOrThrowError().config.sessionExpiredStatusCode === 111) + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + }) + + it('various sameSite values', async () => { + await startST() + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: ' Lax ' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'None ' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'none') + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: ' STRICT ' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'strict') + + resetAll() + } + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'random ' })], + }) + assert(false) + } + catch (err) { + if (err.message !== 'cookie same site must be one of "strict", "lax", or "none"') + throw error + } + + resetAll() + } + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: ' ' })], + }) + assert(false) + } + catch (err) { + if (err.message !== 'cookie same site must be one of "strict", "lax", or "none"') + throw error + } + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'lax' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'none' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'none') + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'strict' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'strict') + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + + resetAll() + } + }) + + it('sameSite none invalid domain values', async () => { + const domainCombinations = [ + ['http://localhost:3000', 'http://supertokensapi.io'], + ['http://127.0.0.1:3000', 'http://supertokensapi.io'], + ['http://supertokens.io', 'http://localhost:8000'], + ['http://supertokens.io', 'http://127.0.0.1:8000'], + ['http://supertokens.io', 'http://supertokensapi.io'], + ] + + for (const domainCombination of domainCombinations) { + let err + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + appName: 'SuperTokens', + websiteDomain: domainCombination[0], + apiDomain: domainCombination[1], + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'none' })], + }) + try { + await Session.createNewSession(mockRequest(), mockResponse(), 'asdf') + } + catch (e) { + err = e + } + assert.ok(err) + assert.strictEqual( + err.message, + 'Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false.', + ) + resetAll() + } + }) + + it('sameSite none valid domain values', async () => { + const domainCombinations = [ + ['http://localhost:3000', 'http://localhost:8000'], + ['http://127.0.0.1:3000', 'http://localhost:8000'], + ['http://localhost:3000', 'http://127.0.0.1:8000'], + ['http://127.0.0.1:3000', 'http://127.0.0.1:8000'], + + ['https://localhost:3000', 'https://localhost:8000'], + ['https://127.0.0.1:3000', 'https://localhost:8000'], + ['https://localhost:3000', 'https://127.0.0.1:8000'], + ['https://127.0.0.1:3000', 'https://127.0.0.1:8000'], + + ['https://supertokens.io', 'https://api.supertokens.io'], + ['https://supertokens.io', 'https://supertokensapi.io'], + + ['http://localhost:3000', 'https://supertokensapi.io'], + ['http://127.0.0.1:3000', 'https://supertokensapi.io'], + ] + + for (const domainCombination of domainCombinations) { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + appName: 'SuperTokens', + websiteDomain: domainCombination[0], + apiDomain: domainCombination[1], + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'none' })], + }) + } + catch (e) { + assert(false) + } + resetAll() + } + }) + + it('testing sessionScope normalisation', async () => { + assert(normaliseSessionScopeOrThrowError('api.example.com') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('http://api.example.com') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('https://api.example.com') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('http://api.example.com?hello=1') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('http://api.example.com/hello') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('http://api.example.com/') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('http://api.example.com:8080') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('http://api.example.com#random2') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('api.example.com/') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('api.example.com#random') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('example.com') === 'example.com') + assert(normaliseSessionScopeOrThrowError('api.example.com/?hello=1&bye=2') === 'api.example.com') + assert(normaliseSessionScopeOrThrowError('localhost.org') === 'localhost.org') + assert(normaliseSessionScopeOrThrowError('localhost') === 'localhost') + assert(normaliseSessionScopeOrThrowError('localhost:8080') === 'localhost') + assert(normaliseSessionScopeOrThrowError('localhost.org') === 'localhost.org') + assert(normaliseSessionScopeOrThrowError('127.0.0.1') === '127.0.0.1') + + assert(normaliseSessionScopeOrThrowError('.api.example.com') === '.api.example.com') + assert(normaliseSessionScopeOrThrowError('.api.example.com/') === '.api.example.com') + assert(normaliseSessionScopeOrThrowError('.api.example.com#random') === '.api.example.com') + assert(normaliseSessionScopeOrThrowError('.example.com') === '.example.com') + assert(normaliseSessionScopeOrThrowError('.api.example.com/?hello=1&bye=2') === '.api.example.com') + assert(normaliseSessionScopeOrThrowError('.localhost.org') === '.localhost.org') + assert(normaliseSessionScopeOrThrowError('.localhost') === 'localhost') + assert(normaliseSessionScopeOrThrowError('.localhost:8080') === 'localhost') + assert(normaliseSessionScopeOrThrowError('.localhost.org') === '.localhost.org') + assert(normaliseSessionScopeOrThrowError('.127.0.0.1') === '127.0.0.1') + + try { + normaliseSessionScopeOrThrowError('http://') + assert(false) + } + catch (err) { + assert(err.message === 'Please provide a valid sessionScope') + } + }) + + it('testing URL path normalisation', async () => { + function normaliseURLPathOrThrowError(input) { + return new NormalisedURLPath(input).getAsStringDangerous() + } + + assert.strictEqual(normaliseURLPathOrThrowError('exists?email=john.doe%40gmail.com'), '/exists') + assert.strictEqual( + normaliseURLPathOrThrowError('/auth/email/exists?email=john.doe%40gmail.com'), + '/auth/email/exists', + ) + assert.strictEqual(normaliseURLPathOrThrowError('exists'), '/exists') + assert.strictEqual(normaliseURLPathOrThrowError('/exists'), '/exists') + assert.strictEqual(normaliseURLPathOrThrowError('/exists?email=john.doe%40gmail.com'), '/exists') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com'), '') + assert.strictEqual(normaliseURLPathOrThrowError('https://api.example.com'), '') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com?hello=1'), '') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com/hello'), '/hello') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com/'), '') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com:8080'), '') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com#random2'), '') + assert.strictEqual(normaliseURLPathOrThrowError('api.example.com/'), '') + assert.strictEqual(normaliseURLPathOrThrowError('api.example.com#random'), '') + assert.strictEqual(normaliseURLPathOrThrowError('.example.com'), '') + assert.strictEqual(normaliseURLPathOrThrowError('api.example.com/?hello=1&bye=2'), '') + + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('http://1.2.3.4/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('1.2.3.4/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('https://api.example.com/one/two/'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com/one/two?hello=1'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com/hello/'), '/hello') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com/one/two/'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com:8080/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('http://api.example.com/one/two#random2'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('api.example.com/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('api.example.com/one/two/#random'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('.example.com/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('api.example.com/one/two?hello=1&bye=2'), '/one/two') + + assert.strictEqual(normaliseURLPathOrThrowError('/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('one/two/'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('/one'), '/one') + assert.strictEqual(normaliseURLPathOrThrowError('one'), '/one') + assert.strictEqual(normaliseURLPathOrThrowError('one/'), '/one') + assert.strictEqual(normaliseURLPathOrThrowError('/one/two/'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('/one/two?hello=1'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('one/two?hello=1'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('/one/two/#random'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('one/two#random'), '/one/two') + + assert.strictEqual(normaliseURLPathOrThrowError('localhost:4000/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('127.0.0.1:4000/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('127.0.0.1/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('https://127.0.0.1:80/one/two'), '/one/two') + assert.strictEqual(normaliseURLPathOrThrowError('/'), '') + assert.strictEqual(normaliseURLPathOrThrowError(''), '') + + assert.strictEqual(normaliseURLPathOrThrowError('/.netlify/functions/api'), '/.netlify/functions/api') + assert.strictEqual(normaliseURLPathOrThrowError('/netlify/.functions/api'), '/netlify/.functions/api') + assert.strictEqual( + normaliseURLPathOrThrowError('app.example.com/.netlify/functions/api'), + '/.netlify/functions/api', + ) + assert.strictEqual( + normaliseURLPathOrThrowError('app.example.com/netlify/.functions/api'), + '/netlify/.functions/api', + ) + assert.strictEqual(normaliseURLPathOrThrowError('/app.example.com'), '/app.example.com') + }) + + it('testing URL domain normalisation', async () => { + function normaliseURLDomainOrThrowError(input) { + return new NormalisedURLDomain(input).getAsStringDangerous() + } + assert(normaliseURLDomainOrThrowError('http://api.example.com') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('https://api.example.com') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('http://api.example.com?hello=1') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('http://api.example.com/hello') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('http://api.example.com/') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('http://api.example.com:8080') === 'http://api.example.com:8080') + assert(normaliseURLDomainOrThrowError('http://api.example.com#random2') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('api.example.com/') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('api.example.com') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('api.example.com#random') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('.example.com') === 'https://example.com') + assert(normaliseURLDomainOrThrowError('api.example.com/?hello=1&bye=2') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('localhost') === 'http://localhost') + assert(normaliseURLDomainOrThrowError('https://localhost') === 'https://localhost') + + assert(normaliseURLDomainOrThrowError('http://api.example.com/one/two') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('http://1.2.3.4/one/two') === 'http://1.2.3.4') + assert(normaliseURLDomainOrThrowError('https://1.2.3.4/one/two') === 'https://1.2.3.4') + assert(normaliseURLDomainOrThrowError('1.2.3.4/one/two') === 'http://1.2.3.4') + assert(normaliseURLDomainOrThrowError('https://api.example.com/one/two/') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('http://api.example.com/one/two?hello=1') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('http://api.example.com/one/two#random2') === 'http://api.example.com') + assert(normaliseURLDomainOrThrowError('api.example.com/one/two') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('api.example.com/one/two/#random') === 'https://api.example.com') + assert(normaliseURLDomainOrThrowError('.example.com/one/two') === 'https://example.com') + assert(normaliseURLDomainOrThrowError('localhost:4000') === 'http://localhost:4000') + assert(normaliseURLDomainOrThrowError('127.0.0.1:4000') === 'http://127.0.0.1:4000') + assert(normaliseURLDomainOrThrowError('127.0.0.1') === 'http://127.0.0.1') + assert(normaliseURLDomainOrThrowError('https://127.0.0.1:80/') === 'https://127.0.0.1:80') + + try { + normaliseURLDomainOrThrowError('/one/two') + assert(false) + } + catch (err) { + assert(err.message === 'Please provide a valid domain name') + } + + try { + normaliseURLDomainOrThrowError('/.netlify/functions/api') + assert(false) + } + catch (err) { + assert(err.message === 'Please provide a valid domain name') + } + }) + + it('various config values', async () => { + await startST() + + { + STExpress.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", accessTokenPath: "/access" })], + }); + assert(SessionRecipe.getInstanceOrThrowError().config.accessTokenPath.getAsStringDangerous() === "/access"); + resetAll(); + } + + { + STExpress.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], + }); + assert(SessionRecipe.getInstanceOrThrowError().config.accessTokenPath.getAsStringDangerous() === ""); + resetAll(); + } + + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/custom/a', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert( + SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous() + === '/custom/a/session/refresh', + ) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert( + SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous() + === '/session/refresh', + ) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert( + SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous() + === '/auth/session/refresh', + ) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + apiKey: 'haha', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert(Querier.apiKey === 'haha') + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/custom', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', sessionExpiredStatusCode: 402 })], + }) + assert(SessionRecipe.getInstanceOrThrowError().config.sessionExpiredStatusCode === 402) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080;try.supertokens.io;try.supertokens.io:8080;localhost:90', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const hosts = Querier.hosts + assert(hosts.length === 4) + + assert(hosts[0].domain.getAsStringDangerous() === 'http://localhost:8080') + assert(hosts[1].domain.getAsStringDangerous() === 'https://try.supertokens.io') + assert(hosts[2].domain.getAsStringDangerous() === 'https://try.supertokens.io:8080') + assert(hosts[3].domain.getAsStringDangerous() === 'http://localhost:90') + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_CUSTOM_HEADER' })], + }) + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'VIA_CUSTOM_HEADER') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'https://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'NONE') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'https://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'NONE') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'NONE') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.com', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'VIA_CUSTOM_HEADER') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'none') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.co.uk', + appName: 'SuperTokens', + websiteDomain: 'supertokens.co.uk', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'NONE') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: '127.0.0.1:3000', + appName: 'SuperTokens', + websiteDomain: '127.0.0.1:9000', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'NONE') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === false) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: '127.0.0.1:3000', + appName: 'SuperTokens', + websiteDomain: '127.0.0.1:9000', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_CUSTOM_HEADER' })], + }) + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'VIA_CUSTOM_HEADER') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === false) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: '127.0.0.1:9000', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'VIA_CUSTOM_HEADER') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'none') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === true) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: '127.0.0.1:3000', + appName: 'SuperTokens', + websiteDomain: '127.0.0.1:9000', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'NONE') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'lax') + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure === false) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'https://127.0.0.1:3000', + appName: 'SuperTokens', + websiteDomain: 'google.com', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'NONE' })], + }) + assert(SessionRecipe.getInstanceOrThrowError().config.antiCsrf === 'NONE') + resetAll() + } + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: '127.0.0.1:3000', + appName: 'SuperTokens', + websiteDomain: 'google.com', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'RANDOM' })], + }) + assert(false) + } + catch (err) { + if (err.message !== 'antiCsrf config must be one of \'NONE\' or \'VIA_CUSTOM_HEADER\' or \'VIA_TOKEN\'') + throw err + } + resetAll() + } + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.test.com:3000', + appName: 'SuperTokens', + websiteDomain: 'google.com', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + assert(false) + } + catch (err) { + assert.strictEqual( + err.message, + 'Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false.', + ) + } + resetAll() + } + + { + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'https://api.test.com:3000', + appName: 'SuperTokens', + websiteDomain: 'google.com', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', cookieSecure: false })], + }) + await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + assert(false) + } + catch (err) { + assert.strictEqual( + err.message, + 'Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false.', + ) + } + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'https://api.test.com:3000', + appName: 'SuperTokens', + websiteDomain: 'google.com', + apiBasePath: 'test/', + websiteBasePath: 'test1/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + getTokenTransferMethod: () => 'header', + cookieSecure: false, + }), + ], + }) + await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'https://localhost', + appName: 'Supertokens', + websiteDomain: 'http://localhost:3000', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSecure) + assert(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite === 'none') + + resetAll() + } + }) + + it('checking for default cookie config', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert.equal(SessionRecipe.getInstanceOrThrowError().config.cookieDomain, undefined) + assert.equal(SessionRecipe.getInstanceOrThrowError().config.cookieSameSite, 'lax') + assert.equal(SessionRecipe.getInstanceOrThrowError().config.cookieSecure, true) + assert.equal( + SessionRecipe.getInstanceOrThrowError().config.refreshTokenPath.getAsStringDangerous(), + '/auth/session/refresh', + ) + assert.equal(SessionRecipe.getInstanceOrThrowError().config.sessionExpiredStatusCode, 401) + }) + + it('Test that the jwt feature is disabled by default', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined) + assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.enable, false) + }) + + it('Test that the jwt feature is disabled when explicitly set to false', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', jwt: { enable: false } })], + }) + assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined) + assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.enable, false) + }) + + it('Test that the jwt feature is enabled when explicitly set to true', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', jwt: { enable: true } })], + }) + + assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined) + assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.enable, true) + }) + + it('Test that the custom jwt property name in access token payload is set correctly in config', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true, propertyNameInAccessTokenPayload: 'customJWTKey' }, + }), + ], + }) + + assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined) + assert.strictEqual( + SessionRecipe.getInstanceOrThrowError().config.jwt.propertyNameInAccessTokenPayload, + 'customJWTKey', + ) + }) + + it('Test that the the jwt property name uses default value when not set in config', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', jwt: { enable: true } })], + }) + + assert.notStrictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt, undefined) + assert.strictEqual(SessionRecipe.getInstanceOrThrowError().config.jwt.propertyNameInAccessTokenPayload, 'jwt') + }) + + it('Test that when setting jwt property name with the same value as the reserved property, init throws an error', async () => { + try { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true, propertyNameInAccessTokenPayload: '_jwtPName' }, + }), + ], + }) + + throw new Error('Init succeeded when it should have failed') + } + catch (e) { + if (e.message !== '_jwtPName is a reserved property name, please use a different key name for the jwt') + throw e + } + }) + + it('testing getTopLevelDomainForSameSiteResolution function', async () => { + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://a.b.test.com'), 'test.com') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('https://a.b.test.com'), 'test.com') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://a.b.test.co.uk'), 'test.co.uk') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://a.b.test.co.uk'), 'test.co.uk') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://test.com'), 'test.com') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('https://test.com'), 'test.com') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://localhost'), 'localhost') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://localhost.org'), 'localhost') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://8.8.8.8'), 'localhost') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://8.8.8.8:8080'), 'localhost') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://localhost:3000'), 'localhost') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('http://test.com:3567'), 'test.com') + assert.strictEqual(getTopLevelDomainForSameSiteResolution('https://test.com:3567'), 'test.com') + }) + + it('apiGatewayPath test', async () => { + await startST() + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiGatewayPath: '/gateway', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.status === 200) + + assert( + SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === '/gateway/auth', + ) + resetAll() + } + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: 'hello', + apiGatewayPath: '/gateway', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/hello/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.status === 200) + + assert( + SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === '/gateway/hello', + ) + resetAll() + } + + { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: 'hello', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/hello/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.status === 200) + + assert(SuperTokens.getInstanceOrThrowError().appInfo.apiBasePath.getAsStringDangerous() === '/hello') + resetAll() + } + }) + + it('checking for empty item in recipeList config', async () => { + await startST() + let errorCaught = true + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' }), , EmailPassword.init()], + }) + errorCaught = false + } + catch (err) { + assert.strictEqual(err.message, 'Please remove empty items from recipeList') + } + assert(errorCaught) + }) + + + it("Check that telemetry is set to true properly", async function () { + await startST(); + STExpress.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init()], + telemetry: true, + }); + + assert(SuperTokens.getInstanceOrThrowError().telemetryEnabled === true); + }); + + it("Check that telemetry is set to false by default", async function () { + await startST(); + STExpress.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init()], + }); + + assert(SuperTokens.getInstanceOrThrowError().telemetryEnabled === false); + }); + + it("Check that telemetry is set to false properly", async function () { + await startST(); + STExpress.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init()], + telemetry: false, + }); + + assert(SuperTokens.getInstanceOrThrowError().telemetryEnabled === false); + }); +}) diff --git a/test/emailpassword/config.test.js b/test/emailpassword/config.test.js deleted file mode 100644 index 82dffd798..000000000 --- a/test/emailpassword/config.test.js +++ /dev/null @@ -1,234 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let utils = require("../../lib/build/recipe/emailpassword/utils"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`configTest: ${printPath("[test/emailpassword/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // test config for emailpassword module - // Failure condition: passing custom data or data of invalid type/ syntax to the module - it("test default config for emailpassword module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init()], - }); - - let emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError(); - - let signUpFeature = emailpassword.config.signUpFeature; - assert(signUpFeature.formFields.length === 2); - assert(signUpFeature.formFields.filter((f) => f.id === "email")[0].optional === false); - assert(signUpFeature.formFields.filter((f) => f.id === "password")[0].optional === false); - assert(signUpFeature.formFields.filter((f) => f.id === "email")[0].validate !== undefined); - assert(signUpFeature.formFields.filter((f) => f.id === "password")[0].validate !== undefined); - - let signInFeature = emailpassword.config.signInFeature; - assert(signInFeature.formFields.length === 2); - assert(signInFeature.formFields.filter((f) => f.id === "email")[0].optional === false); - assert(signInFeature.formFields.filter((f) => f.id === "password")[0].optional === false); - assert(signInFeature.formFields.filter((f) => f.id === "email")[0].validate !== undefined); - assert(signInFeature.formFields.filter((f) => f.id === "password")[0].validate !== undefined); - - let resetPasswordUsingTokenFeature = emailpassword.config.resetPasswordUsingTokenFeature; - - assert(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm.length === 1); - assert(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm[0].id === "email"); - assert(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm.length === 1); - assert(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm[0].id === "password"); - - let emailVerificationFeature = emailpassword.config.emailVerificationFeature; - }); - - // Failure condition: passing data of invalid type/ syntax to the module - it("test config for emailpassword module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "test", - optional: false, - validate: (value) => { - return value + "test"; - }, - }, - ], - }, - }), - ], - }); - - let emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError(); - - let formFields = emailpassword.config.signUpFeature.formFields; - assert(formFields.length === 3); - - let testFormField = await emailpassword.config.signUpFeature.formFields.filter((f) => f.id === "test")[0]; - assert(testFormField !== undefined); - assert(testFormField.optional === false); - assert(testFormField.validate("") === "test"); - }); - - /* - * test validateAndNormaliseUserInput for emailpassword - * - No email / passord validators given should add them - * - Giving optional true in email / password field should be ignored - * - Check that the default password and email validators work fine - */ - it("test that no email/password validators given should add them", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "email", - }, - { - id: "password", - }, - ], - }, - }), - ], - }); - - let emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError(); - assert(emailpassword.config.signUpFeature.formFields[0].validate !== undefined); - assert(emailpassword.config.signUpFeature.formFields[1].validate !== undefined); - }); - - it("test that giving optional true in email / password field should be ignored", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "email", - optional: true, - }, - { - id: "password", - optional: true, - }, - ], - }, - }), - ], - }); - - let emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError(); - assert(!emailpassword.config.signUpFeature.formFields[0].optional); - assert(!emailpassword.config.signUpFeature.formFields[1].optional); - }); - - //Check that the default password and email validators work fine - it("test that default password and email validators work fine", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init()], - }); - - let formFields = await EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields; - - let defaultEmailValidator = formFields.filter((f) => f.id === "email")[0].validate; - assert((await defaultEmailValidator("aaaaa")) === "Email is invalid"); - assert((await defaultEmailValidator("aaaaaa@aaaaaa")) === "Email is invalid"); - assert((await defaultEmailValidator("random User @randomMail.com")) === "Email is invalid"); - assert((await defaultEmailValidator("*@*")) === "Email is invalid"); - assert((await defaultEmailValidator("validmail@gmail.com")) === undefined); - assert((await defaultEmailValidator()) === "Development bug: Please make sure the email field yields a string"); - - let defaultPasswordValidator = formFields.filter((f) => f.id === "password")[0].validate; - assert( - (await defaultPasswordValidator("aaaaa")) === - "Password must contain at least 8 characters, including a number" - ); - assert((await defaultPasswordValidator("aaaaaaaaa")) === "Password must contain at least one number"); - assert((await defaultPasswordValidator("1234*-56*789")) === "Password must contain at least one alphabet"); - assert((await defaultPasswordValidator("validPass123")) === undefined); - assert( - (await defaultPasswordValidator()) === - "Development bug: Please make sure the password field yields a string" - ); - }); -}); diff --git a/test/emailpassword/config.test.ts b/test/emailpassword/config.test.ts new file mode 100644 index 000000000..4a51fa299 --- /dev/null +++ b/test/emailpassword/config.test.ts @@ -0,0 +1,228 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`configTest: ${printPath('[test/emailpassword/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // test config for emailpassword module + // Failure condition: passing custom data or data of invalid type/ syntax to the module + it('test default config for emailpassword module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init()], + }) + + const emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError() + + const signUpFeature = emailpassword.config.signUpFeature + assert(signUpFeature.formFields.length === 2) + assert(signUpFeature.formFields.filter(f => f.id === 'email')[0].optional === false) + assert(signUpFeature.formFields.filter(f => f.id === 'password')[0].optional === false) + assert(signUpFeature.formFields.filter(f => f.id === 'email')[0].validate !== undefined) + assert(signUpFeature.formFields.filter(f => f.id === 'password')[0].validate !== undefined) + + const signInFeature = emailpassword.config.signInFeature + assert(signInFeature.formFields.length === 2) + assert(signInFeature.formFields.filter(f => f.id === 'email')[0].optional === false) + assert(signInFeature.formFields.filter(f => f.id === 'password')[0].optional === false) + assert(signInFeature.formFields.filter(f => f.id === 'email')[0].validate !== undefined) + assert(signInFeature.formFields.filter(f => f.id === 'password')[0].validate !== undefined) + + const resetPasswordUsingTokenFeature = emailpassword.config.resetPasswordUsingTokenFeature + + assert(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm.length === 1) + assert(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm[0].id === 'email') + assert(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm.length === 1) + assert(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm[0].id === 'password') + + const emailVerificationFeature = emailpassword.config.emailVerificationFeature + }) + + // Failure condition: passing data of invalid type/ syntax to the module + it('test config for emailpassword module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'test', + optional: false, + validate: (value) => { + return `${value}test` + }, + }, + ], + }, + }), + ], + }) + + const emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError() + + const formFields = emailpassword.config.signUpFeature.formFields + assert(formFields.length === 3) + + const testFormField = await emailpassword.config.signUpFeature.formFields.filter(f => f.id === 'test')[0] + assert(testFormField !== undefined) + assert(testFormField.optional === false) + assert(testFormField.validate('') === 'test') + }) + + /* + * test validateAndNormaliseUserInput for emailpassword + * - No email / passord validators given should add them + * - Giving optional true in email / password field should be ignored + * - Check that the default password and email validators work fine + */ + it('test that no email/password validators given should add them', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'email', + }, + { + id: 'password', + }, + ], + }, + }), + ], + }) + + const emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError() + assert(emailpassword.config.signUpFeature.formFields[0].validate !== undefined) + assert(emailpassword.config.signUpFeature.formFields[1].validate !== undefined) + }) + + it('test that giving optional true in email / password field should be ignored', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'email', + optional: true, + }, + { + id: 'password', + optional: true, + }, + ], + }, + }), + ], + }) + + const emailpassword = await EmailPasswordRecipe.getInstanceOrThrowError() + assert(!emailpassword.config.signUpFeature.formFields[0].optional) + assert(!emailpassword.config.signUpFeature.formFields[1].optional) + }) + + // Check that the default password and email validators work fine + it('test that default password and email validators work fine', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init()], + }) + + const formFields = await EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields + + const defaultEmailValidator = formFields.filter(f => f.id === 'email')[0].validate + assert((await defaultEmailValidator('aaaaa')) === 'Email is invalid') + assert((await defaultEmailValidator('aaaaaa@aaaaaa')) === 'Email is invalid') + assert((await defaultEmailValidator('random User @randomMail.com')) === 'Email is invalid') + assert((await defaultEmailValidator('*@*')) === 'Email is invalid') + assert((await defaultEmailValidator('validmail@gmail.com')) === undefined) + assert((await defaultEmailValidator()) === 'Development bug: Please make sure the email field yields a string') + + const defaultPasswordValidator = formFields.filter(f => f.id === 'password')[0].validate + assert( + (await defaultPasswordValidator('aaaaa')) + === 'Password must contain at least 8 characters, including a number', + ) + assert((await defaultPasswordValidator('aaaaaaaaa')) === 'Password must contain at least one number') + assert((await defaultPasswordValidator('1234*-56*789')) === 'Password must contain at least one alphabet') + assert((await defaultPasswordValidator('validPass123')) === undefined) + assert( + (await defaultPasswordValidator()) + === 'Development bug: Please make sure the password field yields a string', + ) + }) +}) diff --git a/test/emailpassword/deleteUser.test.js b/test/emailpassword/deleteUser.test.js deleted file mode 100644 index d1c3feeb7..000000000 --- a/test/emailpassword/deleteUser.test.js +++ /dev/null @@ -1,88 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - resetAll, - signUPRequest, - extractInfoFromResponse, -} = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let utils = require("../../lib/build/recipe/emailpassword/utils"); -const express = require("express"); -const request = require("supertest"); -const { default: NormalisedURLPath } = require("../../lib/build/normalisedURLPath"); -let { middleware, errorHandler } = require("../../framework/express"); -let { maxVersion } = require("../../lib/build/utils"); - -describe(`deleteUser: ${printPath("[test/emailpassword/deleteUser.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test deleteUser", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let cdiVersion = await querier.getAPIVersion(); - if (maxVersion("2.10", cdiVersion) === cdiVersion) { - let user = await EmailPassword.signUp("test@example.com", "1234abcd"); - - { - let response = await STExpress.getUsersOldestFirst(); - assert(response.users.length === 1); - } - - await STExpress.deleteUser(user.user.id); - - { - let response = await STExpress.getUsersOldestFirst(); - assert(response.users.length === 0); - } - } - }); -}); diff --git a/test/emailpassword/deleteUser.test.ts b/test/emailpassword/deleteUser.test.ts new file mode 100644 index 000000000..a80c853fd --- /dev/null +++ b/test/emailpassword/deleteUser.test.ts @@ -0,0 +1,76 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import { Querier } from 'supertokens-node/querier' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import { maxVersion } from 'supertokens-node/utils' +import { + cleanST, + killAllST, + printPath, + setupST, + startST, +} from '../utils' + +describe(`deleteUser: ${printPath('[test/emailpassword/deleteUser.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test deleteUser', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const querier = Querier.getNewInstanceOrThrowError(undefined) + const cdiVersion = await querier.getAPIVersion() + if (maxVersion('2.10', cdiVersion) === cdiVersion) { + const user = await EmailPassword.signUp('test@example.com', '1234abcd') + + { + const response = await STExpress.getUsersOldestFirst() + assert(response.users.length === 1) + } + + await STExpress.deleteUser(user.user.id) + + { + const response = await STExpress.getUsersOldestFirst() + assert(response.users.length === 0) + } + } + }) +}) diff --git a/test/emailpassword/emailDelivery.test.js b/test/emailpassword/emailDelivery.test.js deleted file mode 100644 index 0205da7cb..000000000 --- a/test/emailpassword/emailDelivery.test.js +++ /dev/null @@ -1,840 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse, delay } = require("../utils"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let EmailPassword = require("../../recipe/emailpassword"); -const EmailVerification = require("../../recipe/emailverification"); -let { SMTPService } = require("../../recipe/emailpassword/emaildelivery"); -let nock = require("nock"); -let supertest = require("supertest"); -const { middleware, errorHandler } = require("../../framework/express"); -let express = require("express"); - -describe(`emailDelivery: ${printPath("[test/emailpassword/emailDelivery.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await cleanST(); - }); - - it("test default backward compatibility api being called: reset password", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await EmailPassword.signUp("test@example.com", "1234abcd"); - - let appName = undefined; - let email = undefined; - let passwordResetURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/password/reset") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - passwordResetURL = body.passwordResetURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - await delay(2); - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - }); - - it("test default backward compatibility api being called, error message not sent back to user: reset password", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await EmailPassword.signUp("test@example.com", "1234abcd"); - - let appName = undefined; - let email = undefined; - let passwordResetURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/password/reset") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - passwordResetURL = body.passwordResetURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - await delay(2); - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - assert.strictEqual(result.body.status, "OK"); - }); - - it("test backward compatibility: reset password", async function () { - await startST(); - let email = undefined; - let passwordResetURL = undefined; - let timeJoined = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: async (input, passwordResetLink) => { - email = input.email; - passwordResetURL = passwordResetLink; - timeJoined = input.timeJoined; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await EmailPassword.signUp("test@example.com", "1234abcd"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - assert.notStrictEqual(timeJoined, undefined); - }); - - it("test backward compatibility: reset password (non existent user)", async function () { - await startST(); - let functionCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: async (input, passwordResetLink) => { - functionCalled = true; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(functionCalled, false); - - await EmailPassword.signUp("test@example.com", "1234abcd"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(functionCalled, true); - }); - - it("test custom override: reset password", async function () { - await startST(); - let email = undefined; - let passwordResetURL = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - email = input.user.email; - passwordResetURL = input.passwordResetLink; - type = input.type; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await EmailPassword.signUp("test@example.com", "1234abcd"); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/password/reset") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORD_RESET"); - assert.notStrictEqual(passwordResetURL, undefined); - }); - - it("test smtp service: reset password", async function () { - await startST(); - let email = undefined; - let passwordResetURL = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - sendRawEmailCalled = true; - assert.strictEqual(input.body, passwordResetURL); - assert.strictEqual(input.subject, "custom subject"); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORD_RESET"); - passwordResetURL = input.passwordResetLink; - return { - body: input.passwordResetLink, - toEmail: input.user.email, - subject: "custom subject", - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await EmailPassword.signUp("test@example.com", "1234abcd"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.notStrictEqual(passwordResetURL, undefined); - }); - - it("test default backward compatibility api being called: email verify", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await EmailPassword.signUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - let appName = undefined; - let email = undefined; - let emailVerifyURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - emailVerifyURL = body.emailVerifyURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test default backward compatibility api being called, error message not sent back to user: email verify", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await EmailPassword.signUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - let appName = undefined; - let email = undefined; - let emailVerifyURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - emailVerifyURL = body.emailVerifyURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - assert.strictEqual(result.body.status, "OK"); - }); - - it("test backward compatibility: email verify", async function () { - await startST(); - let email = undefined; - let emailVerifyURL = undefined; - let userIdInCb = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { - email = input.email; - userIdInCb = input.id; - emailVerifyURL = emailVerificationURLWithToken; - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await EmailPassword.signUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(userIdInCb, user.user.id); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test custom override: email verify", async function () { - await startST(); - let email = undefined; - let emailVerifyURL = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - email = input.user.email; - emailVerifyURL = input.emailVerifyLink; - type = input.type; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await EmailPassword.signUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "EMAIL_VERIFICATION"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test smtp service: email verify", async function () { - await startST(); - let email = undefined; - let emailVerifyURL = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - sendRawEmailCalled = true; - assert.strictEqual(input.body, emailVerifyURL); - assert.strictEqual(input.subject, "custom subject"); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "EMAIL_VERIFICATION"); - emailVerifyURL = input.emailVerifyLink; - return { - body: input.emailVerifyLink, - toEmail: input.user.email, - subject: "custom subject", - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await EmailPassword.signUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test backward compatibility: reset password and sendEmail override", async function () { - await startST(); - let email = undefined; - let passwordResetURL = undefined; - let timeJoined = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - emailDelivery: { - override: (oI) => { - return { - ...oI, - sendEmail: async function (input) { - input.user.email = "override@example.com"; - return oI.sendEmail(input); - }, - }; - }, - }, - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: async (input, passwordResetLink) => { - email = input.email; - passwordResetURL = passwordResetLink; - timeJoined = input.timeJoined; - }, - }, - }), - Session.init(), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await EmailPassword.signUp("test@example.com", "1234abcd"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "emailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "override@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - assert.notStrictEqual(timeJoined, undefined); - }); -}); diff --git a/test/emailpassword/emailDelivery.test.ts b/test/emailpassword/emailDelivery.test.ts new file mode 100644 index 000000000..951d41e0f --- /dev/null +++ b/test/emailpassword/emailDelivery.test.ts @@ -0,0 +1,842 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { ProcessState } from 'supertokens-node/processState' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import { SMTPService } from 'supertokens-node/recipe/emailpassword/emaildelivery' +import nock from 'nock' +import supertest from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import express from 'express' +import { cleanST, delay, extractInfoFromResponse, killAllST, printPath, setupST, startST } from '../utils' + +describe(`emailDelivery: ${printPath('[test/emailpassword/emailDelivery.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await cleanST() + }) + + it('test default backward compatibility api being called: reset password', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await EmailPassword.signUp('test@example.com', '1234abcd') + + let appName + let email + let passwordResetURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/password/reset') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + passwordResetURL = body.passwordResetURL + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + await delay(2) + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + }) + + it('test default backward compatibility api being called, error message not sent back to user: reset password', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await EmailPassword.signUp('test@example.com', '1234abcd') + + let appName + let email + let passwordResetURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/password/reset') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + passwordResetURL = body.passwordResetURL + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + await delay(2) + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + assert.strictEqual(result.body.status, 'OK') + }) + + it('test backward compatibility: reset password', async () => { + await startST() + let email + let passwordResetURL + let timeJoined + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: async (input, passwordResetLink) => { + email = input.email + passwordResetURL = passwordResetLink + timeJoined = input.timeJoined + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await EmailPassword.signUp('test@example.com', '1234abcd') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + assert.notStrictEqual(timeJoined, undefined) + }) + + it('test backward compatibility: reset password (non existent user)', async () => { + await startST() + let functionCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: async (input, passwordResetLink) => { + functionCalled = true + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(functionCalled, false) + + await EmailPassword.signUp('test@example.com', '1234abcd') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(functionCalled, true) + }) + + it('test custom override: reset password', async () => { + await startST() + let email + let passwordResetURL + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + email = input.user.email + passwordResetURL = input.passwordResetLink + type = input.type + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await EmailPassword.signUp('test@example.com', '1234abcd') + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/password/reset') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORD_RESET') + assert.notStrictEqual(passwordResetURL, undefined) + }) + + it('test smtp service: reset password', async () => { + await startST() + let email + let passwordResetURL + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + sendRawEmailCalled = true + assert.strictEqual(input.body, passwordResetURL) + assert.strictEqual(input.subject, 'custom subject') + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORD_RESET') + passwordResetURL = input.passwordResetLink + return { + body: input.passwordResetLink, + toEmail: input.user.email, + subject: 'custom subject', + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await EmailPassword.signUp('test@example.com', '1234abcd') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.notStrictEqual(passwordResetURL, undefined) + }) + + it('test default backward compatibility api being called: email verify', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await EmailPassword.signUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + let appName + let email + let emailVerifyURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + emailVerifyURL = body.emailVerifyURL + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test default backward compatibility api being called, error message not sent back to user: email verify', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await EmailPassword.signUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + let appName + let email + let emailVerifyURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + emailVerifyURL = body.emailVerifyURL + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + assert.strictEqual(result.body.status, 'OK') + }) + + it('test backward compatibility: email verify', async () => { + await startST() + let email + let emailVerifyURL + let userIdInCb + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { + email = input.email + userIdInCb = input.id + emailVerifyURL = emailVerificationURLWithToken + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await EmailPassword.signUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(userIdInCb, user.user.id) + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test custom override: email verify', async () => { + await startST() + let email + let emailVerifyURL + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + email = input.user.email + emailVerifyURL = input.emailVerifyLink + type = input.type + await oI.sendEmail(input) + }, + } + }, + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await EmailPassword.signUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'EMAIL_VERIFICATION') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test smtp service: email verify', async () => { + await startST() + let email + let emailVerifyURL + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + sendRawEmailCalled = true + assert.strictEqual(input.body, emailVerifyURL) + assert.strictEqual(input.subject, 'custom subject') + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'EMAIL_VERIFICATION') + emailVerifyURL = input.emailVerifyLink + return { + body: input.emailVerifyLink, + toEmail: input.user.email, + subject: 'custom subject', + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await EmailPassword.signUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test backward compatibility: reset password and sendEmail override', async () => { + await startST() + let email + let passwordResetURL + let timeJoined + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + async sendEmail(input) { + input.user.email = 'override@example.com' + return oI.sendEmail(input) + }, + } + }, + }, + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: async (input, passwordResetLink) => { + email = input.email + passwordResetURL = passwordResetLink + timeJoined = input.timeJoined + }, + }, + }), + Session.init(), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await EmailPassword.signUp('test@example.com', '1234abcd') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'emailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'override@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + assert.notStrictEqual(timeJoined, undefined) + }) +}) diff --git a/test/emailpassword/emailExists.test.js b/test/emailpassword/emailExists.test.js deleted file mode 100644 index 642fa3f31..000000000 --- a/test/emailpassword/emailExists.test.js +++ /dev/null @@ -1,466 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let utils = require("../../lib/build/recipe/emailpassword/utils"); -const request = require("supertest"); -const express = require("express"); -let bodyParser = require("body-parser"); -let { middleware, errorHandler } = require("../../framework/express"); - -/* -TODO: - -- Check good input, - - email exists - - email does not exist - - pass an invalid (syntactically) email and check that you get exists: false - - pass an unnormalised email, and check that you get exists true -- Check bad input: - - do not pass email - - pass an array instead of string in the email -*/ - -describe(`emailExists: ${printPath("[test/emailpassword/emailExists.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // disable the email exists API, and check that calling it returns a 404. - it("test that if disableing api, the default email exists API does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailExistsGET: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(response.status === 404); - }); - - // email exists - it("test good input, email exists", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validPass123"); - assert(signUpResponse.status === 200); - assert(JSON.parse(signUpResponse.text).status === "OK"); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === true); - }); - - //email does not exist - it("test good input, email does not exists", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === false); - }); - - //pass an invalid (syntactically) email and check that you get exists: false - it("test email exists a syntactically invalid email", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validPass123"); - assert(signUpResponse.status === 200); - assert(JSON.parse(signUpResponse.text).status === "OK"); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "randomgmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === false); - }); - - //pass an unnormalised email, and check that you get exists true - it("test sending an unnormalised email and you get exists is true", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validPass123"); - assert(signUpResponse.status === 200); - assert(JSON.parse(signUpResponse.text).status === "OK"); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "RaNdOm@gmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === true); - }); - - //do not pass email - it("test bad input, do not pass email", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query() - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide the email as a GET param"); - }); - - // pass an array instead of string in the email - it("test passing an array instead of a string in the email", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validPass123"); - assert(signUpResponse.status === 200); - assert(JSON.parse(signUpResponse.text).status === "OK"); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: ["test1", "test2"], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide the email as a GET param"); - }); - - // email exists - it("test good input, email exists, with bodyParser applied before", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(bodyParser.urlencoded({ extended: true })); - app.use(bodyParser.json()); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validPass123"); - assert(signUpResponse.status === 200); - assert(JSON.parse(signUpResponse.text).status === "OK"); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === true); - }); - - // email exists - it("test good input, email exists, with bodyParser applied after", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(bodyParser.urlencoded({ extended: true })); - app.use(bodyParser.json()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validPass123"); - assert(signUpResponse.status === 200); - assert(JSON.parse(signUpResponse.text).status === "OK"); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === true); - }); -}); diff --git a/test/emailpassword/emailExists.test.ts b/test/emailpassword/emailExists.test.ts new file mode 100644 index 000000000..3cdfa8dd3 --- /dev/null +++ b/test/emailpassword/emailExists.test.ts @@ -0,0 +1,461 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import request from 'supertest' +import express from 'express' +import bodyParser from 'body-parser' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' + +/* +TODO: + +- Check good input, + - email exists + - email does not exist + - pass an invalid (syntactically) email and check that you get exists: false + - pass an unnormalised email, and check that you get exists true +- Check bad input: + - do not pass email + - pass an array instead of string in the email +*/ + +describe(`emailExists: ${printPath('[test/emailpassword/emailExists.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // disable the email exists API, and check that calling it returns a 404. + it('test that if disableing api, the default email exists API does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + emailExistsGET: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(response.status === 404) + }) + + // email exists + it('test good input, email exists', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validPass123') + assert(signUpResponse.status === 200) + assert(JSON.parse(signUpResponse.text).status === 'OK') + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === true) + }) + + // email does not exist + it('test good input, email does not exists', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === false) + }) + + // pass an invalid (syntactically) email and check that you get exists: false + it('test email exists a syntactically invalid email', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validPass123') + assert(signUpResponse.status === 200) + assert(JSON.parse(signUpResponse.text).status === 'OK') + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'randomgmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === false) + }) + + // pass an unnormalised email, and check that you get exists true + it('test sending an unnormalised email and you get exists is true', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validPass123') + assert(signUpResponse.status === 200) + assert(JSON.parse(signUpResponse.text).status === 'OK') + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'RaNdOm@gmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === true) + }) + + // do not pass email + it('test bad input, do not pass email', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query() + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide the email as a GET param') + }) + + // pass an array instead of string in the email + it('test passing an array instead of a string in the email', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validPass123') + assert(signUpResponse.status === 200) + assert(JSON.parse(signUpResponse.text).status === 'OK') + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: ['test1', 'test2'], + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide the email as a GET param') + }) + + // email exists + it('test good input, email exists, with bodyParser applied before', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(bodyParser.urlencoded({ extended: true })) + app.use(bodyParser.json()) + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validPass123') + assert(signUpResponse.status === 200) + assert(JSON.parse(signUpResponse.text).status === 'OK') + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === true) + }) + + // email exists + it('test good input, email exists, with bodyParser applied after', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(bodyParser.urlencoded({ extended: true })) + app.use(bodyParser.json()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validPass123') + assert(signUpResponse.status === 200) + assert(JSON.parse(signUpResponse.text).status === 'OK') + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === true) + }) +}) diff --git a/test/emailpassword/emailverify.test.js b/test/emailpassword/emailverify.test.js deleted file mode 100644 index c9c4a58f9..000000000 --- a/test/emailpassword/emailverify.test.js +++ /dev/null @@ -1,1408 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - resetAll, - signUPRequest, - extractInfoFromResponse, - setKeyValueInConfig, - emailVerifyTokenRequest, -} = require("../utils"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -const EmailVerification = require("../../recipe/emailverification"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -const express = require("express"); -const request = require("supertest"); -let { middleware, errorHandler } = require("../../framework/express"); - -/** - * TODO: (later) in emailVerificationFunctions.ts: - * - (later) check that createAndSendCustomEmail works fine - */ - -describe(`emailverify: ${printPath("[test/emailpassword/emailverify.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - /* - generate token API: - - Call the API with valid input, email not verified - - Call the API with valid input, email verified and test error - - Call the API with no session and see the output (should be 401) - - Call the API with an expired access token and see that try refresh token is returned - - Provide your own email callback and make sure that is called - */ - - // Call the API with valid input, email not verified - it("test the generate token api with valid input, email not verified", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - - assert(JSON.parse(response.text).status === "OK"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - //Call the API with valid input, email verified and test error - it("test the generate token api with valid input, email verified and test error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - let verifyToken = await EmailVerification.createEmailVerificationToken(userId); - await EmailVerification.verifyEmailUsingToken(verifyToken.token); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - - assert(JSON.parse(response.text).status === "EMAIL_ALREADY_VERIFIED_ERROR"); - assert(response.status === 200); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - // Call the API with no session and see the output - it("test the generate token api with valid input, no session and check output", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify/token") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(response.status === 401); - assert(JSON.parse(response.text).message === "unauthorised"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - // Call the API with an expired access token and see that try refresh token is returned - it("test the generate token api with an expired access token and see that try refresh token is returned", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - await new Promise((r) => setTimeout(r, 5000)); - - let response2 = await emailVerifyTokenRequest( - app, - infoFromResponse.accessToken, - infoFromResponse.antiCsrf, - userId - ); - - assert(response2.status === 401); - assert(JSON.parse(response2.text).message === "try refresh token"); - assert(Object.keys(JSON.parse(response2.text)).length === 1); - - let refreshedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + infoFromResponse.refreshToken]) - .set("anti-csrf", infoFromResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let response3 = (response = await emailVerifyTokenRequest( - app, - refreshedResponse.accessToken, - refreshedResponse.antiCsrf, - userId - )); - - assert(response3.status === 200); - assert(JSON.parse(response3.text).status === "OK"); - assert(Object.keys(JSON.parse(response3.text)).length === 1); - }); - - // Provide your own email callback and make sure that is called - it("test that providing your own email callback and make sure it is called", async function () { - await startST(); - - let userInfo = null; - let emailToken = null; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - userInfo = user; - emailToken = emailVerificationURLWithToken; - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - let response2 = await emailVerifyTokenRequest( - app, - infoFromResponse.accessToken, - infoFromResponse.antiCsrf, - userId - ); - - assert(response2.status === 200); - - assert(JSON.parse(response2.text).status === "OK"); - assert(Object.keys(JSON.parse(response2.text)).length === 1); - - assert(userInfo.id === userId); - assert(userInfo.email === "test@gmail.com"); - assert(emailToken !== null); - }); - - /* - email verify API: - POST: - - Call the API with valid input - - Call the API with an invalid token and see the error - - token is not of type string from input - - provide a handlePostEmailVerification callback and make sure it's called on success verification - GET: - - Call the API with valid input - - Call the API with no session and see the error - - Call the API with an expired access token and see that try refresh token is returned - */ - it("test the email verify API with valid input", async function () { - await startST(); - - let token = null; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - }), - EmailPassword.init({}), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(JSON.parse(response.text).status === "OK"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - - assert(JSON.parse(response2.text).status === "OK"); - assert(Object.keys(JSON.parse(response2.text)).length === 1); - }); - - // Call the API with an invalid token and see the error - it("test the email verify API with invalid token and check error", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - response = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token: "randomToken", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - assert(JSON.parse(response.text).status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - // token is not of type string from input - it("test the email verify API with token of not type string", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - response = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token: 2000, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 400); - assert(JSON.parse(response.text).message === "The email verification token must be a string"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - // provide a handlePostEmailVerification callback and make sure it's called on success verification - it("test that the handlePostEmailVerification callback is called on successfull verification, if given", async function () { - await startST(); - - let userInfoFromCallback = null; - let token = null; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - override: { - apis: (oI) => { - return { - ...oI, - verifyEmailPOST: async (token, options) => { - let response = await oI.verifyEmailPOST(token, options); - if (response.status === "OK") { - userInfoFromCallback = response.user; - } - return response; - }, - }; - }, - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(JSON.parse(response.text).status === "OK"); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - - assert(JSON.parse(response2.text).status === "OK"); - assert(Object.keys(JSON.parse(response2.text)).length === 1); - - // wait for the callback to be called... - await new Promise((res) => setTimeout(res, 500)); - - assert(userInfoFromCallback.id === userId); - assert(userInfoFromCallback.email === "test@gmail.com"); - }); - - // Call the API with valid input - it("test the email verify with valid input, using the get method", async function () { - await startST(); - - let token = null; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(JSON.parse(response.text).status === "OK"); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - - assert(JSON.parse(response2.text).status === "OK"); - - let response3 = await new Promise((resolve) => - request(app) - .get("/auth/user/email/verify") - .set("Cookie", ["sAccessToken=" + infoFromResponse.accessToken]) - .set("anti-csrf", infoFromResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - assert(JSON.parse(response3.text).status === "OK"); - assert(JSON.parse(response3.text).isVerified === true); - assert(Object.keys(JSON.parse(response3.text)).length === 2); - }); - - // Call the API with no session and see the error - it("test the email verify with no session, using the get method", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/user/email/verify") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(response.status === 401); - assert(JSON.parse(response.text).message === "unauthorised"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - // Call the API with an expired access token and see that try refresh token is returned - it("test the email verify with an expired access token, using the get method", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); - - let token = null; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(JSON.parse(response.text).status === "OK"); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - - assert(JSON.parse(response2.text).status === "OK"); - - await new Promise((r) => setTimeout(r, 5000)); - - let response3 = await new Promise((resolve) => - request(app) - .get("/auth/user/email/verify") - .set("Cookie", ["sAccessToken=" + infoFromResponse.accessToken]) - .set("anti-csrf", infoFromResponse.antiCsrf) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - assert(response3.status === 401); - assert(JSON.parse(response3.text).message === "try refresh token"); - assert(Object.keys(JSON.parse(response3.text)).length === 1); - - let refreshedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + infoFromResponse.refreshToken]) - .set("anti-csrf", infoFromResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let response4 = await new Promise((resolve) => - request(app) - .get("/auth/user/email/verify") - .set("Cookie", ["sAccessToken=" + refreshedResponse.accessToken]) - .set("anti-csrf", refreshedResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - assert(JSON.parse(response4.text).status === "OK"); - assert(JSON.parse(response4.text).isVerified === true); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - it("test the email verify API with valid input, overriding apis", async function () { - await startST(); - - let user = undefined; - let token = null; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - override: { - apis: (oI) => { - return { - ...oI, - verifyEmailPOST: async (input) => { - let response = await oI.verifyEmailPOST(input); - user = response.user; - return response; - }, - }; - }, - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(response.body.status === "OK"); - assert(response.status === 200); - - let userId = response.body.user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(response.body.status === "OK"); - assert(Object.keys(response.body).length === 1); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res.body); - } - }) - ); - - assert(response2.status === "OK"); - assert(Object.keys(response2).length === 1); - assert.strictEqual(user.id, userId); - assert.strictEqual(user.email, "test@gmail.com"); - }); - - it("test the email verify API with valid input, overriding functions", async function () { - await startST(); - - let user = undefined; - let token = null; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - override: { - functions: (oI) => { - return { - ...oI, - verifyEmailUsingToken: async (input) => { - let response = await oI.verifyEmailUsingToken(input); - user = response.user; - return response; - }, - }; - }, - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(response.body.status === "OK"); - assert(response.status === 200); - - let userId = response.body.user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(response.body.status === "OK"); - assert(Object.keys(response.body).length === 1); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res.body); - } - }) - ); - - assert(response2.status === "OK"); - assert(Object.keys(response2).length === 1); - assert.strictEqual(user.id, userId); - assert.strictEqual(user.email, "test@gmail.com"); - }); - - it("test the email verify API with valid input, overriding apis throws error", async function () { - await startST(); - - let user = undefined; - let token = null; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - override: { - apis: (oI) => { - return { - ...oI, - verifyEmailPOST: async (input) => { - let response = await oI.verifyEmailPOST(input); - user = response.user; - throw { - error: "verify email error", - }; - }, - }; - }, - }, - }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, req, res, next) => { - res.json({ - customError: true, - ...err, - }); - }); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(response.body.status === "OK"); - assert(response.status === 200); - - let userId = response.body.user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(response.body.status === "OK"); - assert(Object.keys(response.body).length === 1); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(response2, { customError: true, error: "verify email error" }); - assert.strictEqual(user.id, userId); - assert.strictEqual(user.email, "test@gmail.com"); - }); - - it("test the email verify API with valid input, overriding functions throws error", async function () { - await startST(); - - let user = undefined; - let token = null; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - override: { - functions: (oI) => { - return { - ...oI, - verifyEmailUsingToken: async (input) => { - let response = await oI.verifyEmailUsingToken(input); - user = response.user; - throw { - error: "verify email error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, req, res, next) => { - res.json({ - customError: true, - ...err, - }); - }); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(response.body.status === "OK"); - assert(response.status === 200); - - let userId = response.body.user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert(response.body.status === "OK"); - assert(Object.keys(response.body).length === 1); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(response2, { customError: true, error: "verify email error" }); - assert.strictEqual(user.id, userId); - assert.strictEqual(user.email, "test@gmail.com"); - }); - - it("test the generate token api with valid input, and then remove token", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ], - }); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - let verifyToken = await EmailVerification.createEmailVerificationToken(userId, "test@gmail.com"); - - await EmailVerification.revokeEmailVerificationTokens(userId); - - { - let response = await EmailVerification.verifyEmailUsingToken(verifyToken.token); - assert.equal(response.status, "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"); - } - }); - - it("test the generate token api with valid input, verify and then unverify email", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ], - }); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - const verifyToken = await EmailVerification.createEmailVerificationToken(userId); - - await EmailVerification.verifyEmailUsingToken(verifyToken.token); - - assert(await EmailVerification.isEmailVerified(userId)); - - await EmailVerification.unverifyEmail(userId); - - assert(!(await EmailVerification.isEmailVerified(userId))); - }); - - it("test the email verify API with deleted user", async function () { - await startST(); - - let token = null; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - token = emailVerificationURLWithToken.split("?token=")[1].split("&rid=")[0]; - }, - }), - Session.init({ - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.10") !== apiVersion) { - return this.skip(); - } - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert.strictEqual(response.body.status, "OK"); - assert.strictEqual(response.status, 200); - - let userId = response.body.user.id; - let infoFromResponse = extractInfoFromResponse(response); - await STExpress.deleteUser(userId); - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - assert.strictEqual(response.statusCode, 401); - assert.deepStrictEqual(response.body, { message: "unauthorised" }); - }); - - it("should work with getEmailForUserId returning errors", async () => { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - getEmailForUserId: (userId) => - userId === "testuserid" - ? { status: "EMAIL_DOES_NOT_EXIST_ERROR" } - : { status: "UNKNOWN_USER_ID_ERROR" }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - assert.deepStrictEqual(await EmailVerification.revokeEmailVerificationTokens("testuserid"), { status: "OK" }); - - let caughtError; - try { - await EmailVerification.revokeEmailVerificationTokens("nouserid"); - } catch (err) { - caughtError = err; - } - - assert.ok(caughtError); - assert.strictEqual(caughtError.message, "Unknown User ID provided without email"); - }); - - it("test that generate email verification token API updates session claims", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => {}, - }), - Session.init({ - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.10") !== apiVersion) { - return this.skip(); - } - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert.strictEqual(response.body.status, "OK"); - assert.strictEqual(response.status, 200); - - let userId = response.body.user.id; - let infoFromResponse = extractInfoFromResponse(response); - let antiCsrfToken = infoFromResponse.antiCsrf; - let token = await EmailVerification.createEmailVerificationToken(userId); - await EmailVerification.verifyEmailUsingToken(token.token); - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId); - infoFromResponse = extractInfoFromResponse(response); - assert.strictEqual(response.statusCode, 200); - assert.deepStrictEqual(response.body.status, "EMAIL_ALREADY_VERIFIED_ERROR"); - let frontendInfo = JSON.parse(new Buffer.from(infoFromResponse.frontToken, "base64").toString()); - assert.strictEqual(frontendInfo.up["st-ev"].v, true); - - // calling the API again should not modify the access token again - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId); - let infoFromResponse2 = extractInfoFromResponse(response); - assert.strictEqual(response.statusCode, 200); - assert.deepStrictEqual(response.body.status, "EMAIL_ALREADY_VERIFIED_ERROR"); - assert.strictEqual(infoFromResponse2.frontToken, undefined); - - // now we mark the email as unverified and try again - await EmailVerification.unverifyEmail(userId); - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId); - infoFromResponse = extractInfoFromResponse(response); - assert.strictEqual(response.statusCode, 200); - assert.deepStrictEqual(response.body.status, "OK"); - frontendInfo = JSON.parse(new Buffer.from(infoFromResponse.frontToken, "base64").toString()); - assert.strictEqual(frontendInfo.up["st-ev"].v, false); - - // calling the API again should not modify the access token again - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId); - infoFromResponse2 = extractInfoFromResponse(response); - assert.strictEqual(response.statusCode, 200); - assert.deepStrictEqual(response.body.status, "OK"); - assert.strictEqual(infoFromResponse2.frontToken, undefined); - }); -}); diff --git a/test/emailpassword/emailverify.test.ts b/test/emailpassword/emailverify.test.ts new file mode 100644 index 000000000..400e713ed --- /dev/null +++ b/test/emailpassword/emailverify.test.ts @@ -0,0 +1,1404 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import { ProcessState } from 'supertokens-node/processState' +import { maxVersion } from 'supertokens-node/utils' +import { Querier } from 'supertokens-node/querier' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + emailVerifyTokenRequest, + extractInfoFromResponse, + killAllST, + printPath, + setKeyValueInConfig, + setupST, + signUPRequest, + startST, +} from '../utils' + +/** + * TODO: (later) in emailVerificationFunctions.ts: + * - (later) check that createAndSendCustomEmail works fine + */ + +describe(`emailverify: ${printPath('[test/emailpassword/emailverify.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + /* + generate token API: + - Call the API with valid input, email not verified + - Call the API with valid input, email verified and test error + - Call the API with no session and see the output (should be 401) + - Call the API with an expired access token and see that try refresh token is returned + - Provide your own email callback and make sure that is called + */ + + // Call the API with valid input, email not verified + it('test the generate token api with valid input, email not verified', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + + assert(JSON.parse(response.text).status === 'OK') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + // Call the API with valid input, email verified and test error + it('test the generate token api with valid input, email verified and test error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + const verifyToken = await EmailVerification.createEmailVerificationToken(userId) + await EmailVerification.verifyEmailUsingToken(verifyToken.token) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + + assert(JSON.parse(response.text).status === 'EMAIL_ALREADY_VERIFIED_ERROR') + assert(response.status === 200) + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + // Call the API with no session and see the output + it('test the generate token api with valid input, no session and check output', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify/token') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(response.status === 401) + assert(JSON.parse(response.text).message === 'unauthorised') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + // Call the API with an expired access token and see that try refresh token is returned + it('test the generate token api with an expired access token and see that try refresh token is returned', async () => { + await setKeyValueInConfig('access_token_validity', 2) + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + await new Promise(r => setTimeout(r, 5000)) + + const response2 = await emailVerifyTokenRequest( + app, + infoFromResponse.accessToken, + infoFromResponse.antiCsrf, + userId, + ) + + assert(response2.status === 401) + assert(JSON.parse(response2.text).message === 'try refresh token') + assert(Object.keys(JSON.parse(response2.text)).length === 1) + + const refreshedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${infoFromResponse.refreshToken}`]) + .set('anti-csrf', infoFromResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const response3 = (response = await emailVerifyTokenRequest( + app, + refreshedResponse.accessToken, + refreshedResponse.antiCsrf, + userId, + )) + + assert(response3.status === 200) + assert(JSON.parse(response3.text).status === 'OK') + assert(Object.keys(JSON.parse(response3.text)).length === 1) + }) + + // Provide your own email callback and make sure that is called + it('test that providing your own email callback and make sure it is called', async () => { + await startST() + + let userInfo = null + let emailToken = null + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + userInfo = user + emailToken = emailVerificationURLWithToken + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + const response2 = await emailVerifyTokenRequest( + app, + infoFromResponse.accessToken, + infoFromResponse.antiCsrf, + userId, + ) + + assert(response2.status === 200) + + assert(JSON.parse(response2.text).status === 'OK') + assert(Object.keys(JSON.parse(response2.text)).length === 1) + + assert(userInfo.id === userId) + assert(userInfo.email === 'test@gmail.com') + assert(emailToken !== null) + }) + + /* + email verify API: + POST: + - Call the API with valid input + - Call the API with an invalid token and see the error + - token is not of type string from input + - provide a handlePostEmailVerification callback and make sure it's called on success verification + GET: + - Call the API with valid input + - Call the API with no session and see the error + - Call the API with an expired access token and see that try refresh token is returned + */ + it('test the email verify API with valid input', async () => { + await startST() + + let token = null + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + }), + EmailPassword.init({}), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(JSON.parse(response.text).status === 'OK') + assert(Object.keys(JSON.parse(response.text)).length === 1) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + + assert(JSON.parse(response2.text).status === 'OK') + assert(Object.keys(JSON.parse(response2.text)).length === 1) + }) + + // Call the API with an invalid token and see the error + it('test the email verify API with invalid token and check error', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token: 'randomToken', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + assert(JSON.parse(response.text).status === 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + // token is not of type string from input + it('test the email verify API with token of not type string', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token: 2000, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 400) + assert(JSON.parse(response.text).message === 'The email verification token must be a string') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + // provide a handlePostEmailVerification callback and make sure it's called on success verification + it('test that the handlePostEmailVerification callback is called on successfull verification, if given', async () => { + await startST() + + let userInfoFromCallback = null + let token = null + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + override: { + apis: (oI) => { + return { + ...oI, + verifyEmailPOST: async (token, options) => { + const response = await oI.verifyEmailPOST(token, options) + if (response.status === 'OK') + userInfoFromCallback = response.user + + return response + }, + } + }, + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(JSON.parse(response.text).status === 'OK') + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + + assert(JSON.parse(response2.text).status === 'OK') + assert(Object.keys(JSON.parse(response2.text)).length === 1) + + // wait for the callback to be called... + await new Promise(res => setTimeout(res, 500)) + + assert(userInfoFromCallback.id === userId) + assert(userInfoFromCallback.email === 'test@gmail.com') + }) + + // Call the API with valid input + it('test the email verify with valid input, using the get method', async () => { + await startST() + + let token = null + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(JSON.parse(response.text).status === 'OK') + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + + assert(JSON.parse(response2.text).status === 'OK') + + const response3 = await new Promise(resolve => + request(app) + .get('/auth/user/email/verify') + .set('Cookie', [`sAccessToken=${infoFromResponse.accessToken}`]) + .set('anti-csrf', infoFromResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + assert(JSON.parse(response3.text).status === 'OK') + assert(JSON.parse(response3.text).isVerified === true) + assert(Object.keys(JSON.parse(response3.text)).length === 2) + }) + + // Call the API with no session and see the error + it('test the email verify with no session, using the get method', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/user/email/verify') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(response.status === 401) + assert(JSON.parse(response.text).message === 'unauthorised') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + // Call the API with an expired access token and see that try refresh token is returned + it('test the email verify with an expired access token, using the get method', async () => { + await setKeyValueInConfig('access_token_validity', 2) + await startST() + + let token = null + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(JSON.parse(response.text).status === 'OK') + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + + assert(JSON.parse(response2.text).status === 'OK') + + await new Promise(r => setTimeout(r, 5000)) + + const response3 = await new Promise(resolve => + request(app) + .get('/auth/user/email/verify') + .set('Cookie', [`sAccessToken=${infoFromResponse.accessToken}`]) + .set('anti-csrf', infoFromResponse.antiCsrf) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + assert(response3.status === 401) + assert(JSON.parse(response3.text).message === 'try refresh token') + assert(Object.keys(JSON.parse(response3.text)).length === 1) + + const refreshedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${infoFromResponse.refreshToken}`]) + .set('anti-csrf', infoFromResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const response4 = await new Promise(resolve => + request(app) + .get('/auth/user/email/verify') + .set('Cookie', [`sAccessToken=${refreshedResponse.accessToken}`]) + .set('anti-csrf', refreshedResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + assert(JSON.parse(response4.text).status === 'OK') + assert(JSON.parse(response4.text).isVerified === true) + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + it('test the email verify API with valid input, overriding apis', async () => { + await startST() + + let user + let token = null + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + override: { + apis: (oI) => { + return { + ...oI, + verifyEmailPOST: async (input) => { + const response = await oI.verifyEmailPOST(input) + user = response.user + return response + }, + } + }, + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(response.body.status === 'OK') + assert(response.status === 200) + + const userId = response.body.user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(response.body.status === 'OK') + assert(Object.keys(response.body).length === 1) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res.body) + }), + ) + + assert(response2.status === 'OK') + assert(Object.keys(response2).length === 1) + assert.strictEqual(user.id, userId) + assert.strictEqual(user.email, 'test@gmail.com') + }) + + it('test the email verify API with valid input, overriding functions', async () => { + await startST() + + let user + let token = null + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + override: { + functions: (oI) => { + return { + ...oI, + verifyEmailUsingToken: async (input) => { + const response = await oI.verifyEmailUsingToken(input) + user = response.user + return response + }, + } + }, + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(response.body.status === 'OK') + assert(response.status === 200) + + const userId = response.body.user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(response.body.status === 'OK') + assert(Object.keys(response.body).length === 1) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res.body) + }), + ) + + assert(response2.status === 'OK') + assert(Object.keys(response2).length === 1) + assert.strictEqual(user.id, userId) + assert.strictEqual(user.email, 'test@gmail.com') + }) + + it('test the email verify API with valid input, overriding apis throws error', async () => { + await startST() + + let user + let token = null + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + override: { + apis: (oI) => { + return { + ...oI, + verifyEmailPOST: async (input) => { + const response = await oI.verifyEmailPOST(input) + user = response.user + throw { + error: 'verify email error', + } + }, + } + }, + }, + }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, req, res, next) => { + res.json({ + customError: true, + ...err, + }) + }) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(response.body.status === 'OK') + assert(response.status === 200) + + const userId = response.body.user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(response.body.status === 'OK') + assert(Object.keys(response.body).length === 1) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(response2, { customError: true, error: 'verify email error' }) + assert.strictEqual(user.id, userId) + assert.strictEqual(user.email, 'test@gmail.com') + }) + + it('test the email verify API with valid input, overriding functions throws error', async () => { + await startST() + + let user + let token = null + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + override: { + functions: (oI) => { + return { + ...oI, + verifyEmailUsingToken: async (input) => { + const response = await oI.verifyEmailUsingToken(input) + user = response.user + throw { + error: 'verify email error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, req, res, next) => { + res.json({ + customError: true, + ...err, + }) + }) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(response.body.status === 'OK') + assert(response.status === 200) + + const userId = response.body.user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert(response.body.status === 'OK') + assert(Object.keys(response.body).length === 1) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(response2, { customError: true, error: 'verify email error' }) + assert.strictEqual(user.id, userId) + assert.strictEqual(user.email, 'test@gmail.com') + }) + + it('test the generate token api with valid input, and then remove token', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ], + }) + + const apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + const verifyToken = await EmailVerification.createEmailVerificationToken(userId, 'test@gmail.com') + + await EmailVerification.revokeEmailVerificationTokens(userId) + + { + const response = await EmailVerification.verifyEmailUsingToken(verifyToken.token) + assert.equal(response.status, 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR') + } + }) + + it('test the generate token api with valid input, verify and then unverify email', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ], + }) + + const apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + const verifyToken = await EmailVerification.createEmailVerificationToken(userId) + + await EmailVerification.verifyEmailUsingToken(verifyToken.token) + + assert(await EmailVerification.isEmailVerified(userId)) + + await EmailVerification.unverifyEmail(userId) + + assert(!(await EmailVerification.isEmailVerified(userId))) + }) + + it('test the email verify API with deleted user', async function () { + await startST() + + let token = null + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + token = emailVerificationURLWithToken.split('?token=')[1].split('&rid=')[0] + }, + }), + Session.init({ + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.10') !== apiVersion) + return this.skip() + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert.strictEqual(response.body.status, 'OK') + assert.strictEqual(response.status, 200) + + const userId = response.body.user.id + const infoFromResponse = extractInfoFromResponse(response) + await STExpress.deleteUser(userId) + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + assert.strictEqual(response.statusCode, 401) + assert.deepStrictEqual(response.body, { message: 'unauthorised' }) + }) + + it('should work with getEmailForUserId returning errors', async () => { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + getEmailForUserId: userId => + userId === 'testuserid' + ? { status: 'EMAIL_DOES_NOT_EXIST_ERROR' } + : { status: 'UNKNOWN_USER_ID_ERROR' }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + assert.deepStrictEqual(await EmailVerification.revokeEmailVerificationTokens('testuserid'), { status: 'OK' }) + + let caughtError + try { + await EmailVerification.revokeEmailVerificationTokens('nouserid') + } + catch (err) { + caughtError = err + } + + assert.ok(caughtError) + assert.strictEqual(caughtError.message, 'Unknown User ID provided without email') + }) + + it('test that generate email verification token API updates session claims', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => {}, + }), + Session.init({ + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.10') !== apiVersion) + return this.skip() + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert.strictEqual(response.body.status, 'OK') + assert.strictEqual(response.status, 200) + + const userId = response.body.user.id + let infoFromResponse = extractInfoFromResponse(response) + const antiCsrfToken = infoFromResponse.antiCsrf + const token = await EmailVerification.createEmailVerificationToken(userId) + await EmailVerification.verifyEmailUsingToken(token.token) + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId) + infoFromResponse = extractInfoFromResponse(response) + assert.strictEqual(response.statusCode, 200) + assert.deepStrictEqual(response.body.status, 'EMAIL_ALREADY_VERIFIED_ERROR') + let frontendInfo = JSON.parse(new Buffer.from(infoFromResponse.frontToken, 'base64').toString()) + assert.strictEqual(frontendInfo.up['st-ev'].v, true) + + // calling the API again should not modify the access token again + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId) + let infoFromResponse2 = extractInfoFromResponse(response) + assert.strictEqual(response.statusCode, 200) + assert.deepStrictEqual(response.body.status, 'EMAIL_ALREADY_VERIFIED_ERROR') + assert.strictEqual(infoFromResponse2.frontToken, undefined) + + // now we mark the email as unverified and try again + await EmailVerification.unverifyEmail(userId) + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId) + infoFromResponse = extractInfoFromResponse(response) + assert.strictEqual(response.statusCode, 200) + assert.deepStrictEqual(response.body.status, 'OK') + frontendInfo = JSON.parse(new Buffer.from(infoFromResponse.frontToken, 'base64').toString()) + assert.strictEqual(frontendInfo.up['st-ev'].v, false) + + // calling the API again should not modify the access token again + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, antiCsrfToken, userId) + infoFromResponse2 = extractInfoFromResponse(response) + assert.strictEqual(response.statusCode, 200) + assert.deepStrictEqual(response.body.status, 'OK') + assert.strictEqual(infoFromResponse2.frontToken, undefined) + }) +}) diff --git a/test/emailpassword/formFieldValidator.test.js b/test/emailpassword/formFieldValidator.test.js deleted file mode 100644 index 26c4618c9..000000000 --- a/test/emailpassword/formFieldValidator.test.js +++ /dev/null @@ -1,60 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -let { defaultPasswordValidator, defaultEmailValidator } = require("../../lib/build/recipe/emailpassword/utils"); -let assert = require("assert"); -const { printPath } = require("../utils"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`formFieldValidator: ${printPath("[test/emailpassword/formFieldValidator.test.js]")}`, function () { - it("checking email validator", async function () { - assert((await defaultEmailValidator("test@supertokens.io")) === undefined); - assert((await defaultEmailValidator("nsdafa@gmail.com")) === undefined); - assert((await defaultEmailValidator("fewf3r_fdkj@gmaildsfa.co.uk")) === undefined); - assert((await defaultEmailValidator("dafk.adfa@gmail.com")) === undefined); - assert((await defaultEmailValidator("skjlblc3f3@fnldsks.co")) === undefined); - assert((await defaultEmailValidator("sdkjfnas34@gmail.com.c")) === "Email is invalid"); - assert((await defaultEmailValidator("d@c")) === "Email is invalid"); - assert((await defaultEmailValidator("fasd")) === "Email is invalid"); - assert((await defaultEmailValidator("dfa@@@abc.com")) === "Email is invalid"); - assert((await defaultEmailValidator("")) === "Email is invalid"); - }); - - it("checking password validator", async function () { - assert((await defaultPasswordValidator("dsknfkf38H")) === undefined); - assert((await defaultPasswordValidator("lasdkf*787~sdfskj")) === undefined); - assert((await defaultPasswordValidator("L0493434505")) === undefined); - assert((await defaultPasswordValidator("3453342422L")) === undefined); - assert((await defaultPasswordValidator("1sdfsdfsdfsd")) === undefined); - assert((await defaultPasswordValidator("dksjnlvsnl2")) === undefined); - assert((await defaultPasswordValidator("abcgftr8")) === undefined); - assert((await defaultPasswordValidator("abc!@#$%^&*()gftr8")) === undefined); - assert((await defaultPasswordValidator(" dskj3")) === undefined); - assert((await defaultPasswordValidator(" dsk 3")) === undefined); - assert((await defaultPasswordValidator(" d3 ")) === undefined); - - assert( - (await defaultPasswordValidator("asd")) === - "Password must contain at least 8 characters, including a number" - ); - assert( - (await defaultPasswordValidator( - "asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4" - )) === "Password's length must be lesser than 100 characters" - ); - assert((await defaultPasswordValidator("ascdvsdfvsIUOO")) === "Password must contain at least one number"); - assert((await defaultPasswordValidator("234235234523")) === "Password must contain at least one alphabet"); - }); -}); diff --git a/test/emailpassword/formFieldValidator.test.ts b/test/emailpassword/formFieldValidator.test.ts new file mode 100644 index 000000000..c45ae1ece --- /dev/null +++ b/test/emailpassword/formFieldValidator.test.ts @@ -0,0 +1,60 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { defaultEmailValidator, defaultPasswordValidator } from 'supertokens-node/recipe/emailpassword/utils' +import { describe, it } from 'vitest' +import { printPath } from '../utils' + +describe(`formFieldValidator: ${printPath('[test/emailpassword/formFieldValidator.test.ts]')}`, () => { + it('checking email validator', async () => { + assert((await defaultEmailValidator('test@supertokens.io')) === undefined) + assert((await defaultEmailValidator('nsdafa@gmail.com')) === undefined) + assert((await defaultEmailValidator('fewf3r_fdkj@gmaildsfa.co.uk')) === undefined) + assert((await defaultEmailValidator('dafk.adfa@gmail.com')) === undefined) + assert((await defaultEmailValidator('skjlblc3f3@fnldsks.co')) === undefined) + assert((await defaultEmailValidator('sdkjfnas34@gmail.com.c')) === 'Email is invalid') + assert((await defaultEmailValidator('d@c')) === 'Email is invalid') + assert((await defaultEmailValidator('fasd')) === 'Email is invalid') + assert((await defaultEmailValidator('dfa@@@abc.com')) === 'Email is invalid') + assert((await defaultEmailValidator('')) === 'Email is invalid') + }) + + it('checking password validator', async () => { + assert((await defaultPasswordValidator('dsknfkf38H')) === undefined) + assert((await defaultPasswordValidator('lasdkf*787~sdfskj')) === undefined) + assert((await defaultPasswordValidator('L0493434505')) === undefined) + assert((await defaultPasswordValidator('3453342422L')) === undefined) + assert((await defaultPasswordValidator('1sdfsdfsdfsd')) === undefined) + assert((await defaultPasswordValidator('dksjnlvsnl2')) === undefined) + assert((await defaultPasswordValidator('abcgftr8')) === undefined) + assert((await defaultPasswordValidator('abc!@#$%^&*()gftr8')) === undefined) + assert((await defaultPasswordValidator(' dskj3')) === undefined) + assert((await defaultPasswordValidator(' dsk 3')) === undefined) + assert((await defaultPasswordValidator(' d3 ')) === undefined) + + assert( + (await defaultPasswordValidator('asd')) + === 'Password must contain at least 8 characters, including a number', + ) + assert( + (await defaultPasswordValidator( + 'asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4asdfdefrg4', + )) === 'Password\'s length must be lesser than 100 characters', + ) + assert((await defaultPasswordValidator('ascdvsdfvsIUOO')) === 'Password must contain at least one number') + assert((await defaultPasswordValidator('234235234523')) === 'Password must contain at least one alphabet') + }) +}) diff --git a/test/emailpassword/override.test.js b/test/emailpassword/override.test.js deleted file mode 100644 index 191d314e7..000000000 --- a/test/emailpassword/override.test.js +++ /dev/null @@ -1,538 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let utils = require("../../lib/build/recipe/emailpassword/utils"); -const express = require("express"); -const request = require("supertest"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`overrideTest: ${printPath("[test/emailpassword/override.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("overriding functions tests", async () => { - await startST(); - let user = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - functions: (oI) => { - return { - ...oI, - signUp: async (input) => { - let response = await oI.signUp(input); - if (response.status === "OK") { - user = response.user; - } - return response; - }, - signIn: async (input) => { - let response = await oI.signIn(input); - if (response.status === "OK") { - user = response.user; - } - return response; - }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); - }); - - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signInResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: signInResponse.user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); - }); - - it("overriding api tests", async () => { - await startST(); - let user = undefined; - let emailExists = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - signUpPOST: async (input) => { - let response = await oI.signUpPOST(input); - if (response.status === "OK") { - user = response.user; - } - return response; - }, - signInPOST: async (input) => { - let response = await oI.signInPOST(input); - if (response.status === "OK") { - user = response.user; - } - return response; - }, - emailExistsGET: async (input) => { - let response = await oI.emailExistsGET(input); - emailExists = response.exists; - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); - }); - - let emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.strictEqual(emailExistsResponse.exists, false); - assert.strictEqual(emailExists, false); - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body.user, user); - - emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.strictEqual(emailExistsResponse.exists, true); - assert.strictEqual(emailExists, true); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signInResponse.user, user); - }); - - it("overriding functions tests, throws error", async () => { - await startST(); - let user = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - functions: (oI) => { - return { - ...oI, - signUp: async (input) => { - let response = await oI.signUp(input); - user = response.user; - throw { - error: "signup error", - }; - }, - signIn: async (input) => { - await oI.signIn(input); - throw { - error: "signin error", - }; - }, - getUserById: async (input) => { - await oI.getUserById(input); - throw { - error: "get user error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res, next) => { - try { - let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); - } catch (err) { - next(err); - } - }); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body, { error: "signup error", customError: true }); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(userByIdResponse, { error: "get user error", customError: true }); - }); - - it("overriding api tests, throws error", async () => { - await startST(); - let user = undefined; - let emailExists = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - signUpPOST: async (input) => { - let response = await oI.signUpPOST(input); - user = response.user; - throw { - error: "signup error", - }; - }, - signInPOST: async (input) => { - await oI.signInPOST(input); - throw { - error: "signin error", - }; - }, - emailExistsGET: async (input) => { - let response = await oI.emailExistsGET(input); - emailExists = response.exists; - throw { - error: "email exists error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await EmailPassword.getUserById(userId)); - }); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - assert.deepStrictEqual(emailExistsResponse, { error: "email exists error", customError: true }); - assert.strictEqual(emailExists, false); - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body, { error: "signup error", customError: true }); - - emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.deepStrictEqual(emailExistsResponse, { error: "email exists error", customError: true }); - assert.strictEqual(emailExists, true); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - }); -}); diff --git a/test/emailpassword/override.test.ts b/test/emailpassword/override.test.ts new file mode 100644 index 000000000..6471b1223 --- /dev/null +++ b/test/emailpassword/override.test.ts @@ -0,0 +1,534 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' + +describe(`overrideTest: ${printPath('[test/emailpassword/override.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('overriding functions tests', async () => { + await startST() + let user + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (oI) => { + return { + ...oI, + signUp: async (input) => { + const response = await oI.signUp(input) + if (response.status === 'OK') + user = response.user + + return response + }, + signIn: async (input) => { + const response = await oI.signIn(input) + if (response.status === 'OK') + user = response.user + + return response + }, + getUserById: async (input) => { + const response = await oI.getUserById(input) + user = response + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await EmailPassword.getUserById(userId)) + }) + + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse.body.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signInResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: signInResponse.user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(userByIdResponse, user) + }) + + it('overriding api tests', async () => { + await startST() + let user + let emailExists + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + signUpPOST: async (input) => { + const response = await oI.signUpPOST(input) + if (response.status === 'OK') + user = response.user + + return response + }, + signInPOST: async (input) => { + const response = await oI.signInPOST(input) + if (response.status === 'OK') + user = response.user + + return response + }, + emailExistsGET: async (input) => { + const response = await oI.emailExistsGET(input) + emailExists = response.exists + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await EmailPassword.getUserById(userId)) + }) + + let emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.strictEqual(emailExistsResponse.exists, false) + assert.strictEqual(emailExists, false) + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse.body.user, user) + + emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.strictEqual(emailExistsResponse.exists, true) + assert.strictEqual(emailExists, true) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signInResponse.user, user) + }) + + it('overriding functions tests, throws error', async () => { + await startST() + let user + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (oI) => { + return { + ...oI, + signUp: async (input) => { + const response = await oI.signUp(input) + user = response.user + throw { + error: 'signup error', + } + }, + signIn: async (input) => { + await oI.signIn(input) + throw { + error: 'signin error', + } + }, + getUserById: async (input) => { + await oI.getUserById(input) + throw { + error: 'get user error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res, next) => { + try { + const userId = req.query.userId + res.json(await EmailPassword.getUserById(userId)) + } + catch (err) { + next(err) + } + }) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse.body, { error: 'signup error', customError: true }) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(userByIdResponse, { error: 'get user error', customError: true }) + }) + + it('overriding api tests, throws error', async () => { + await startST() + let user + let emailExists + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + signUpPOST: async (input) => { + const response = await oI.signUpPOST(input) + user = response.user + throw { + error: 'signup error', + } + }, + signInPOST: async (input) => { + await oI.signInPOST(input) + throw { + error: 'signin error', + } + }, + emailExistsGET: async (input) => { + const response = await oI.emailExistsGET(input) + emailExists = response.exists + throw { + error: 'email exists error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await EmailPassword.getUserById(userId)) + }) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + let emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + assert.deepStrictEqual(emailExistsResponse, { error: 'email exists error', customError: true }) + assert.strictEqual(emailExists, false) + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse.body, { error: 'signup error', customError: true }) + + emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.deepStrictEqual(emailExistsResponse, { error: 'email exists error', customError: true }) + assert.strictEqual(emailExists, true) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + }) +}) diff --git a/test/emailpassword/passwordreset.test.js b/test/emailpassword/passwordreset.test.js deleted file mode 100644 index f993d2d3e..000000000 --- a/test/emailpassword/passwordreset.test.js +++ /dev/null @@ -1,489 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let generatePasswordResetToken = require("../../lib/build/recipe/emailpassword/api/generatePasswordResetToken").default; -let passwordReset = require("../../lib/build/recipe/emailpassword/api/passwordReset").default; -const express = require("express"); -const request = require("supertest"); -let { middleware, errorHandler } = require("../../framework/express"); -let { maxVersion } = require("../../lib/build/utils"); - -/** - * TODO: (later) in passwordResetFunctions.ts: - * - (later) check that createAndSendCustomEmail works fine - * TODO: generate token API: - * - (later) Call the createResetPasswordToken function with valid input - * - (later) Call the createResetPasswordToken with unknown userId and test error thrown - * TODO: password reset API: - * - (later) Call the resetPasswordUsingToken function with valid input - * - (later) Call the resetPasswordUsingToken with an invalid token and see the error - * - (later) token is not of type string from input - */ - -describe(`passwordreset: ${printPath("[test/emailpassword/passwordreset.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - /* - * generate token API: - * - email validation checks - * - non existent email should return "OK" with a pause > 300MS - * - check that the generated password reset link is correct - */ - it("test email validation checks in generate token API", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init()], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset/token") - .send({ - formFields: [ - { - id: "email", - value: "random", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.body.status === "FIELD_ERROR"); - assert(response.body.formFields.length === 1); - assert(response.body.formFields[0].error === "Email is invalid"); - assert(response.body.formFields[0].id === "email"); - }); - - it("test that generated password link is correct", async function () { - await startST(); - - let resetURL = ""; - let tokenInfo = ""; - let ridInfo = ""; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: (user, passwordResetURLWithToken) => { - resetURL = passwordResetURLWithToken.split("?")[0]; - tokenInfo = passwordResetURLWithToken.split("?")[1].split("&")[0]; - ridInfo = passwordResetURLWithToken.split("?")[1].split("&")[1]; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset/token") - .send({ - formFields: [ - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(resetURL === "https://supertokens.io/auth/reset-password"); - assert(tokenInfo.startsWith("token=")); - assert(ridInfo.startsWith("rid=emailpassword")); - }); - - /* - * password reset API: - * - password validation checks - * - token is missing from input - * - invalid token in input - * - input is valid, check that password has changed (call sign in) - */ - it("test password validation", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init()], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset") - .send({ - formFields: [ - { - id: "password", - value: "invalid", - }, - ], - token: "randomToken", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields[0].error === "Password must contain at least 8 characters, including a number"); - assert(response.formFields[0].id === "password"); - - response = await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - ], - token: "randomToken", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status !== "FIELD_ERROR"); - }); - - it("test token missing from input", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init()], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - ], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide the password reset token"); - }); - - it("test invalid token input", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init()], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - ], - token: "invalidToken", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR"); - }); - - it("test valid token input and passoword has changed", async function () { - await startST(); - - let passwordResetUserId = undefined; - let token = ""; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - passwordResetPOST: async function (input) { - let resp = await oI.passwordResetPOST(input); - if (resp.userId !== undefined) { - passwordResetUserId = resp.userId; - } - return resp; - }, - }; - }, - }, - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: (user, passwordResetURLWithToken) => { - token = passwordResetURLWithToken.split("?")[1].split("&")[0].split("=")[1]; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userInfo = JSON.parse(response.text).user; - - await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset/token") - .send({ - formFields: [ - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - await new Promise((resolve) => - request(app) - .post("/auth/user/password/reset") - .send({ - formFields: [ - { - id: "password", - value: "validpass12345", - }, - ], - token, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - let currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(currCDIVersion, "2.12") === currCDIVersion) { - assert(passwordResetUserId !== undefined && passwordResetUserId === userInfo.id); - } else { - assert(passwordResetUserId === undefined); - } - - let failureResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(failureResponse.status === "WRONG_CREDENTIALS_ERROR"); - - let successResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass12345", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(successResponse.status === "OK"); - assert(successResponse.user.id === userInfo.id); - assert(successResponse.user.email === userInfo.email); - }); -}); diff --git a/test/emailpassword/passwordreset.test.ts b/test/emailpassword/passwordreset.test.ts new file mode 100644 index 000000000..eceaddac8 --- /dev/null +++ b/test/emailpassword/passwordreset.test.ts @@ -0,0 +1,482 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import { Querier } from 'supertokens-node/querier' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' + +/** + * TODO: (later) in passwordResetFunctions.ts: + * - (later) check that createAndSendCustomEmail works fine + * TODO: generate token API: + * - (later) Call the createResetPasswordToken function with valid input + * - (later) Call the createResetPasswordToken with unknown userId and test error thrown + * TODO: password reset API: + * - (later) Call the resetPasswordUsingToken function with valid input + * - (later) Call the resetPasswordUsingToken with an invalid token and see the error + * - (later) token is not of type string from input + */ + +describe(`passwordreset: ${printPath('[test/emailpassword/passwordreset.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + /* + * generate token API: + * - email validation checks + * - non existent email should return "OK" with a pause > 300MS + * - check that the generated password reset link is correct + */ + it('test email validation checks in generate token API', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init()], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/password/reset/token') + .send({ + formFields: [ + { + id: 'email', + value: 'random', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.body.status === 'FIELD_ERROR') + assert(response.body.formFields.length === 1) + assert(response.body.formFields[0].error === 'Email is invalid') + assert(response.body.formFields[0].id === 'email') + }) + + it('test that generated password link is correct', async () => { + await startST() + + let resetURL = '' + let tokenInfo = '' + let ridInfo = '' + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: (user, passwordResetURLWithToken) => { + resetURL = passwordResetURLWithToken.split('?')[0] + tokenInfo = passwordResetURLWithToken.split('?')[1].split('&')[0] + ridInfo = passwordResetURLWithToken.split('?')[1].split('&')[1] + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + await new Promise(resolve => + request(app) + .post('/auth/user/password/reset/token') + .send({ + formFields: [ + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(resetURL === 'https://supertokens.io/auth/reset-password') + assert(tokenInfo.startsWith('token=')) + assert(ridInfo.startsWith('rid=emailpassword')) + }) + + /* + * password reset API: + * - password validation checks + * - token is missing from input + * - invalid token in input + * - input is valid, check that password has changed (call sign in) + */ + it('test password validation', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init()], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await new Promise(resolve => + request(app) + .post('/auth/user/password/reset') + .send({ + formFields: [ + { + id: 'password', + value: 'invalid', + }, + ], + token: 'randomToken', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields[0].error === 'Password must contain at least 8 characters, including a number') + assert(response.formFields[0].id === 'password') + + response = await new Promise(resolve => + request(app) + .post('/auth/user/password/reset') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + ], + token: 'randomToken', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status !== 'FIELD_ERROR') + }) + + it('test token missing from input', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init()], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/password/reset') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide the password reset token') + }) + + it('test invalid token input', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init()], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/password/reset') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + ], + token: 'invalidToken', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'RESET_PASSWORD_INVALID_TOKEN_ERROR') + }) + + it('test valid token input and passoword has changed', async () => { + await startST() + + let passwordResetUserId + let token = '' + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + async passwordResetPOST(input) { + const resp = await oI.passwordResetPOST(input) + if (resp.userId !== undefined) + passwordResetUserId = resp.userId + + return resp + }, + } + }, + }, + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: (user, passwordResetURLWithToken) => { + token = passwordResetURLWithToken.split('?')[1].split('&')[0].split('=')[1] + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userInfo = JSON.parse(response.text).user + + await new Promise(resolve => + request(app) + .post('/auth/user/password/reset/token') + .send({ + formFields: [ + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + await new Promise(resolve => + request(app) + .post('/auth/user/password/reset') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass12345', + }, + ], + token, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(currCDIVersion, '2.12') === currCDIVersion) + assert(passwordResetUserId !== undefined && passwordResetUserId === userInfo.id) + + else + assert(passwordResetUserId === undefined) + + const failureResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(failureResponse.status === 'WRONG_CREDENTIALS_ERROR') + + const successResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass12345', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(successResponse.status === 'OK') + assert(successResponse.user.id === userInfo.id) + assert(successResponse.user.email === userInfo.email) + }) +}) diff --git a/test/emailpassword/signinFeature.test.js b/test/emailpassword/signinFeature.test.js deleted file mode 100644 index 637d808ef..000000000 --- a/test/emailpassword/signinFeature.test.js +++ /dev/null @@ -1,1101 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - resetAll, - signUPRequest, - extractInfoFromResponse, -} = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let utils = require("../../lib/build/recipe/emailpassword/utils"); -const express = require("express"); -const request = require("supertest"); -const { default: NormalisedURLPath } = require("../../lib/build/normalisedURLPath"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signinFeature: ${printPath("[test/emailpassword/signinFeature.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // check if disabling api, the default signin API does not work - you get a 404 - /* - */ - it("test that disabling api, the default signin API does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - signInPOST: undefined, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 404); - }); - - /* - * test signInAPI for: - * - it works when the input is fine (sign up, and then sign in and check you get the user's info) - * - throws an error if the email does not match - * - throws an error if the password is incorrect - */ - - /* - Failure condition: - Setting invalid email or password values in the request body when sending a request to /signin - */ - it("test singinAPI works when input is fine", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - /* - Setting the email value in form field as random@gmail.com causes the test to fail - */ - it("test singinAPI throws an error when email does not match", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let invalidEmailResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "ran@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(invalidEmailResponse.status === "WRONG_CREDENTIALS_ERROR"); - }); - - // throws an error if the password is incorrect - /* - passing the correct password "validpass123" causes the test to fail - */ - it("test singinAPI throws an error if password is incorrect", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let invalidPasswordResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass12345", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(invalidPasswordResponse.status === "WRONG_CREDENTIALS_ERROR"); - }); - - /* - * pass a bad input to the /signin API and test that it throws a 400 error. - * - Not a JSON - * - No POST body - * - Input is JSON, but wrong structure. - */ - /* - Failure condition: - setting valid JSON body to /singin API - */ - it("test bad input, not a JSON to /signin API", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send("hello") - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "Missing input param: formFields"); - }); - - /* - Failure condition: - setting valid formFields JSON body to /singin API - */ - it("test bad input, no POST body to /signin API", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "Missing input param: formFields"); - }); - - /* - Failure condition: - setting valid JSON body to /singin API - */ - it("test bad input, input is Json but incorrect structure to /signin API", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - randomKey: "randomValue", - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "Missing input param: formFields"); - }); - - // Make sure that a successful sign in yields a session - /* - Passing invalid credentials to the /signin API fails the test - */ - it("test that a successfull signin yields a session", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let cookies = extractInfoFromResponse(response); - assert(cookies.accessToken !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.antiCsrf !== undefined); - assert(cookies.accessTokenExpiry !== undefined); - assert(cookies.refreshTokenExpiry !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - assert(cookies.frontToken !== undefined); - }); - - /* - * formField validation testing: - * - Provide custom email validators to sign up and make sure they are applied to sign in - * - Provide custom password validators to sign up and make sure they are not applied to sign in. - * - Test password field validation error. The result should not be a FORM_FIELD_ERROR, but should be WRONG_CREDENTIALS_ERROR - * - Test email field validation error - * - Input formFields has no email field - * - Input formFields has no password field - * - Provide invalid (wrong syntax) email and wrong password, and you should get form field error - */ - /* - having the email start with "test" (requierment of the custom validator) will cause the test to fail - */ - it("test custom email validators to sign up and make sure they are applied to sign in", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "email", - validate: (value) => { - if (value.startsWith("test")) { - return undefined; - } - return "email does not start with test"; - }, - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "testrandom@gmail.com", "validpass123"); - - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields[0].error === "email does not start with test"); - assert(response.formFields[0].id === "email"); - }); - - //- Provide custom password validators to sign up and make sure they are not applied to sign in. - /* - sending the correct password "valid" will cause the test to fail - */ - it("test custom password validators to sign up and make sure they are applied to sign in", async function () { - await startST(); - - let failsValidatorCtr = 0; - let passesValidatorCtr = 0; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "password", - validate: (value) => { - if (value.length <= 5) { - passesValidatorCtr++; - return undefined; - } - failsValidatorCtr++; - return "password is greater than 5 characters"; - }, - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "valid"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - assert(passesValidatorCtr === 1); - assert(failsValidatorCtr === 0); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "invalid", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - - assert(response.status === "WRONG_CREDENTIALS_ERROR"); - assert(failsValidatorCtr === 0); - assert(passesValidatorCtr === 1); - }); - - // Test password field validation error. The result should not be a FORM_FIELD_ERROR, but should be WRONG_CREDENTIALS_ERROR - /* - sending the correct password to the /signin API will cause the test to fail - */ - it("test password field validation error", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "invalidpass", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - assert(response.status === "WRONG_CREDENTIALS_ERROR"); - }); - - // Test email field validation error - //sending the correct email to the /signin API will cause the test to fail - - it("test email field validation error", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "randomgmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields[0].error === "Email is invalid"); - assert(response.formFields[0].id === "email"); - }); - - // Input formFields has no email field - //passing the email field in formFields will cause the test to fail - it("test formFields has no email field", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - ], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - assert(response.message === "Are you sending too many / too few formFields?"); - }); - - // Input formFields has no password field - //passing the password field in formFields will cause the test to fail - it("test formFields has no password field", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - assert(response.message === "Are you sending too many / too few formFields?"); - }); - - // Provide invalid (wrong syntax) email and wrong password, and you should get form field error - /* - passing email with valid syntax and correct password will cause the test to fail - */ - it("test invalid email and wrong password", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "invalid", - }, - { - id: "email", - value: "randomgmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields.length === 1); - assert(response.formFields[0].error === "Email is invalid"); - assert(response.formFields[0].id === "email"); - }); - - /* - * Test getUserByEmail - * - User does not exist - * - User exists - */ - it("test getUserByEmail when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let emailpassword = EmailPasswordRecipe.getInstanceOrThrowError(); - - assert((await EmailPassword.getUserByEmail("random@gmail.com")) === undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let signUpUserInfo = JSON.parse(signUpResponse.text).user; - let userInfo = await EmailPassword.getUserByEmail("random@gmail.com"); - - assert(userInfo.email === signUpUserInfo.email); - assert(userInfo.id === signUpUserInfo.id); - }); - - /* - * Test getUserById - * - User does not exist - * - User exists - */ - it("test getUserById when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let emailpassword = EmailPasswordRecipe.getInstanceOrThrowError(); - - assert((await EmailPassword.getUserById("randomID")) === undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let signUpUserInfo = JSON.parse(signUpResponse.text).user; - let userInfo = await EmailPassword.getUserById(signUpUserInfo.id); - - assert(userInfo.email === signUpUserInfo.email); - assert(userInfo.id === signUpUserInfo.id); - }); - - it("test the handlePostSignIn function", async function () { - await startST(); - - let customUser = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - signInPOST: async (formFields, options) => { - let response = await oI.signInPOST(formFields, options); - if (response.status === "OK") { - customUser = response.user; - } - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(customUser !== undefined); - assert.deepStrictEqual(response.user, customUser); - }); -}); diff --git a/test/emailpassword/signinFeature.test.ts b/test/emailpassword/signinFeature.test.ts new file mode 100644 index 000000000..d3e8ece68 --- /dev/null +++ b/test/emailpassword/signinFeature.test.ts @@ -0,0 +1,1100 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword/recipe' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractInfoFromResponse, + killAllST, + printPath, + setupST, + signUPRequest, + startST, +} from '../utils' + +describe(`signinFeature: ${printPath('[test/emailpassword/signinFeature.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check if disabling api, the default signin API does not work - you get a 404 + /* + */ + it('test that disabling api, the default signin API does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + signInPOST: undefined, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 404) + }) + + /* + * test signInAPI for: + * - it works when the input is fine (sign up, and then sign in and check you get the user's info) + * - throws an error if the email does not match + * - throws an error if the password is incorrect + */ + + /* + Failure condition: + Setting invalid email or password values in the request body when sending a request to /signin + */ + it('test singinAPI works when input is fine', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + /* + Setting the email value in form field as random@gmail.com causes the test to fail + */ + it('test singinAPI throws an error when email does not match', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const invalidEmailResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'ran@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(invalidEmailResponse.status === 'WRONG_CREDENTIALS_ERROR') + }) + + // throws an error if the password is incorrect + /* + passing the correct password "validpass123" causes the test to fail + */ + it('test singinAPI throws an error if password is incorrect', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const invalidPasswordResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass12345', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(invalidPasswordResponse.status === 'WRONG_CREDENTIALS_ERROR') + }) + + /* + * pass a bad input to the /signin API and test that it throws a 400 error. + * - Not a JSON + * - No POST body + * - Input is JSON, but wrong structure. + */ + /* + Failure condition: + setting valid JSON body to /singin API + */ + it('test bad input, not a JSON to /signin API', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send('hello') + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'Missing input param: formFields') + }) + + /* + Failure condition: + setting valid formFields JSON body to /singin API + */ + it('test bad input, no POST body to /signin API', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'Missing input param: formFields') + }) + + /* + Failure condition: + setting valid JSON body to /singin API + */ + it('test bad input, input is Json but incorrect structure to /signin API', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + randomKey: 'randomValue', + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'Missing input param: formFields') + }) + + // Make sure that a successful sign in yields a session + /* + Passing invalid credentials to the /signin API fails the test + */ + it('test that a successfull signin yields a session', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const cookies = extractInfoFromResponse(response) + assert(cookies.accessToken !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.antiCsrf !== undefined) + assert(cookies.accessTokenExpiry !== undefined) + assert(cookies.refreshTokenExpiry !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + assert(cookies.frontToken !== undefined) + }) + + /* + * formField validation testing: + * - Provide custom email validators to sign up and make sure they are applied to sign in + * - Provide custom password validators to sign up and make sure they are not applied to sign in. + * - Test password field validation error. The result should not be a FORM_FIELD_ERROR, but should be WRONG_CREDENTIALS_ERROR + * - Test email field validation error + * - Input formFields has no email field + * - Input formFields has no password field + * - Provide invalid (wrong syntax) email and wrong password, and you should get form field error + */ + /* + having the email start with "test" (requierment of the custom validator) will cause the test to fail + */ + it('test custom email validators to sign up and make sure they are applied to sign in', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'email', + validate: (value) => { + if (value.startsWith('test')) + return undefined + + return 'email does not start with test' + }, + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'testrandom@gmail.com', 'validpass123') + + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields[0].error === 'email does not start with test') + assert(response.formFields[0].id === 'email') + }) + + // - Provide custom password validators to sign up and make sure they are not applied to sign in. + /* + sending the correct password "valid" will cause the test to fail + */ + it('test custom password validators to sign up and make sure they are applied to sign in', async () => { + await startST() + + let failsValidatorCtr = 0 + let passesValidatorCtr = 0 + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'password', + validate: (value) => { + if (value.length <= 5) { + passesValidatorCtr++ + return undefined + } + failsValidatorCtr++ + return 'password is greater than 5 characters' + }, + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'valid') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + assert(passesValidatorCtr === 1) + assert(failsValidatorCtr === 0) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'invalid', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'WRONG_CREDENTIALS_ERROR') + assert(failsValidatorCtr === 0) + assert(passesValidatorCtr === 1) + }) + + // Test password field validation error. The result should not be a FORM_FIELD_ERROR, but should be WRONG_CREDENTIALS_ERROR + /* + sending the correct password to the /signin API will cause the test to fail + */ + it('test password field validation error', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'invalidpass', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'WRONG_CREDENTIALS_ERROR') + }) + + // Test email field validation error + // sending the correct email to the /signin API will cause the test to fail + + it('test email field validation error', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'randomgmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields[0].error === 'Email is invalid') + assert(response.formFields[0].id === 'email') + }) + + // Input formFields has no email field + // passing the email field in formFields will cause the test to fail + it('test formFields has no email field', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Are you sending too many / too few formFields?') + }) + + // Input formFields has no password field + // passing the password field in formFields will cause the test to fail + it('test formFields has no password field', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Are you sending too many / too few formFields?') + }) + + // Provide invalid (wrong syntax) email and wrong password, and you should get form field error + /* + passing email with valid syntax and correct password will cause the test to fail + */ + it('test invalid email and wrong password', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'invalid', + }, + { + id: 'email', + value: 'randomgmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields.length === 1) + assert(response.formFields[0].error === 'Email is invalid') + assert(response.formFields[0].id === 'email') + }) + + /* + * Test getUserByEmail + * - User does not exist + * - User exists + */ + it('test getUserByEmail when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const emailpassword = EmailPasswordRecipe.getInstanceOrThrowError() + + assert((await EmailPassword.getUserByEmail('random@gmail.com')) === undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const signUpUserInfo = JSON.parse(signUpResponse.text).user + const userInfo = await EmailPassword.getUserByEmail('random@gmail.com') + + assert(userInfo.email === signUpUserInfo.email) + assert(userInfo.id === signUpUserInfo.id) + }) + + /* + * Test getUserById + * - User does not exist + * - User exists + */ + it('test getUserById when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const emailpassword = EmailPasswordRecipe.getInstanceOrThrowError() + + assert((await EmailPassword.getUserById('randomID')) === undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const signUpUserInfo = JSON.parse(signUpResponse.text).user + const userInfo = await EmailPassword.getUserById(signUpUserInfo.id) + + assert(userInfo.email === signUpUserInfo.email) + assert(userInfo.id === signUpUserInfo.id) + }) + + it('test the handlePostSignIn function', async () => { + await startST() + + let customUser + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + signInPOST: async (formFields, options) => { + const response = await oI.signInPOST(formFields, options) + if (response.status === 'OK') + customUser = response.user + + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(customUser !== undefined) + assert.deepStrictEqual(response.user, customUser) + }) +}) diff --git a/test/emailpassword/signoutFeature.test.js b/test/emailpassword/signoutFeature.test.js deleted file mode 100644 index b1c3918b7..000000000 --- a/test/emailpassword/signoutFeature.test.js +++ /dev/null @@ -1,298 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - resetAll, - signUPRequest, - extractInfoFromResponse, - setKeyValueInConfig, -} = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let utils = require("../../lib/build/recipe/emailpassword/utils"); -const express = require("express"); -const request = require("supertest"); -const { default: NormalisedURLPath } = require("../../lib/build/normalisedURLPath"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signoutFeature: ${printPath("[test/emailpassword/signoutFeature.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // Test the default route and it should revoke the session (with clearing the cookies) - it("test the default route and it should revoke the session", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let res = extractInfoFromResponse(response); - - let response2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - assert.strictEqual(response2.antiCsrf, undefined); - assert.strictEqual(response2.accessToken, ""); - assert.strictEqual(response2.refreshToken, ""); - assert.strictEqual(response2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.accessTokenDomain, undefined); - assert.strictEqual(response2.refreshTokenDomain, undefined); - assert.strictEqual(response2.frontToken, "remove"); - }); - - // Disable default route and test that that API returns 404 - it("test that disabling default route and calling the API returns 404", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "emailpassword") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 404); - }); - - // Call the API without a session and it should return "OK" - it("test that calling the API without a session should return OK", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - assert(response.header["set-cookie"] === undefined); - }); - - //Call the API with an expired access token, refresh, and call the API again to get OK and clear cookies - it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let res = extractInfoFromResponse(response); - - await new Promise((r) => setTimeout(r, 5000)); - - let signOutResponse = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(signOutResponse.status, 401); - assert.strictEqual(signOutResponse.body.message, "try refresh token"); - - let refreshedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - signOutResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + refreshedResponse.accessToken]) - .set("anti-csrf", refreshedResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(signOutResponse.antiCsrf === undefined); - assert(signOutResponse.accessToken === ""); - assert(signOutResponse.refreshToken === ""); - assert(signOutResponse.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(signOutResponse.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(signOutResponse.accessTokenDomain === undefined); - assert(signOutResponse.refreshTokenDomain === undefined); - }); -}); diff --git a/test/emailpassword/signoutFeature.test.ts b/test/emailpassword/signoutFeature.test.ts new file mode 100644 index 000000000..1a787ce8a --- /dev/null +++ b/test/emailpassword/signoutFeature.test.ts @@ -0,0 +1,289 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractInfoFromResponse, + killAllST, + printPath, + setKeyValueInConfig, + setupST, + signUPRequest, + startST, +} from '../utils' + +describe(`signoutFeature: ${printPath('[test/emailpassword/signoutFeature.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // Test the default route and it should revoke the session (with clearing the cookies) + it('test the default route and it should revoke the session', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const res = extractInfoFromResponse(response) + + const response2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + assert.strictEqual(response2.antiCsrf, undefined) + assert.strictEqual(response2.accessToken, '') + assert.strictEqual(response2.refreshToken, '') + assert.strictEqual(response2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.accessTokenDomain, undefined) + assert.strictEqual(response2.refreshTokenDomain, undefined) + assert.strictEqual(response2.frontToken, 'remove') + }) + + // Disable default route and test that that API returns 404 + it('test that disabling default route and calling the API returns 404', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'emailpassword') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 404) + }) + + // Call the API without a session and it should return "OK" + it('test that calling the API without a session should return OK', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + assert(response.header['set-cookie'] === undefined) + }) + + // Call the API with an expired access token, refresh, and call the API again to get OK and clear cookies + it('test that signout API reutrns try refresh token, refresh session and signout should return OK', async () => { + await setKeyValueInConfig('access_token_validity', 2) + + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const res = extractInfoFromResponse(response) + + await new Promise(r => setTimeout(r, 5000)) + + let signOutResponse = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(signOutResponse.status, 401) + assert.strictEqual(signOutResponse.body.message, 'try refresh token') + + const refreshedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + signOutResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${refreshedResponse.accessToken}`]) + .set('anti-csrf', refreshedResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(signOutResponse.antiCsrf === undefined) + assert(signOutResponse.accessToken === '') + assert(signOutResponse.refreshToken === '') + assert(signOutResponse.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(signOutResponse.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(signOutResponse.accessTokenDomain === undefined) + assert(signOutResponse.refreshTokenDomain === undefined) + }) +}) diff --git a/test/emailpassword/signupFeature.test.js b/test/emailpassword/signupFeature.test.js deleted file mode 100644 index 37fdf3071..000000000 --- a/test/emailpassword/signupFeature.test.js +++ /dev/null @@ -1,1461 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - resetAll, - signUPRequest, - extractInfoFromResponse, -} = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let { normaliseURLPathOrThrowError } = require("../../lib/build/normalisedURLPath"); -let { normaliseURLDomainOrThrowError } = require("../../lib/build/normalisedURLDomain"); -let { normaliseSessionScopeOrThrowError } = require("../../lib/build/recipe/session/utils"); -const { Querier } = require("../../lib/build/querier"); -let EmailPassword = require("../../recipe/emailpassword"); -let EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword/recipe").default; -let utils = require("../../lib/build/recipe/emailpassword/utils"); -const express = require("express"); -const request = require("supertest"); -const { default: NormalisedURLPath } = require("../../lib/build/normalisedURLPath"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signupFeature: ${printPath("[test/emailpassword/signupFeature.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // * check if disable api, the default signup API does not work - you get a 404 - it("test that if disable api, the default signup API does not work", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - signUpPOST: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(response.status === 404); - }); - - /* - * test signUpAPI for: - * - it works when the input is fine (sign up, get user id, get email of that user and check the input email is same as the one used for sign up) - * - throws an error in case of duplicate email. - */ - - it("test signUpAPI works when input is fine", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userInfo = JSON.parse(response.text).user; - assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); - }); - - it("test signUpAPI throws an error in case of a duplicate email", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userInfo = JSON.parse(response.text).user; - assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); - - response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(response.status === 200); - let responseInfo = JSON.parse(response.text); - - assert(responseInfo.status === "FIELD_ERROR"); - assert(responseInfo.formFields.length === 1); - assert(responseInfo.formFields[0].id === "email"); - assert(responseInfo.formFields[0].error === "This email already exists. Please sign in instead."); - }); - - it("test signUpAPI throws an error for email and password with invalid syntax", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "randomgmail.com", "invalidpass"); - assert(response.status === 200); - let responseInfo = JSON.parse(response.text); - - assert(responseInfo.status === "FIELD_ERROR"); - assert(responseInfo.formFields.length === 2); - assert(responseInfo.formFields.filter((f) => f.id === "email")[0].error === "Email is invalid"); - assert( - responseInfo.formFields.filter((f) => f.id === "password")[0].error === - "Password must contain at least one number" - ); - }); - - /* pass a bad input to the /signup API and test that it throws a 400 error. - * - Not a JSON - * - No POST body - * - Input is JSON, but wrong structure. - * - formFields is not an array - * - formFields does not exist - * - formField elements have no id or no value field - * */ - it("test bad input, not a JSON to /signup API", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send("hello") - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "Missing input param: formFields"); - }); - - it("test bad input, no POST body to /signup API", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "Missing input param: formFields"); - }); - - it("test bad input, Input is JSON, but wrong structure to /signup API", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - randomKey: "randomValue", - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "Missing input param: formFields"); - }); - - it("test bad input, formFields is not an array in /signup API", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: { - randomKey: "randomValue", - }, - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "formFields must be an array"); - }); - - it("test bad input, formField elements have no id or no value field in /signup API", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let badInputResponse = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - randomKey: "randomValue", - }, - { - randomKey2: "randomValue2", - }, - ], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(badInputResponse.message === "All elements of formFields must contain an 'id' and 'value' field"); - }); - - //* Make sure that a successful sign up yields a session - it("test that a successful signup yields a session", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let cookies = extractInfoFromResponse(signUpResponse); - assert(cookies.accessToken !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.antiCsrf !== undefined); - assert(cookies.accessTokenExpiry !== undefined); - assert(cookies.refreshTokenExpiry !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - assert(cookies.frontToken !== undefined); - }); - - /* - * providing the handlePostSignup should work: - * - If not provided by the user, it should not result in an error - * - If provided by the user, and custom fields are there, only those should be sent - * - If provided by the user, and no custom fields are there, then the formFields param must sbe empty - */ - - //If not provided by the user, it should not result in an error - - it("test that if not provided by the user, it should not result in an error", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "testValue", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); - }); - - //- If provided by the user, and custom fields are there, only those should be sent - it("test that if provided by the user, and custom fields are there, only those are sent, using handlePostSignUp", async function () { - await startST(); - - let customFormFields = ""; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - }, - ], - }, - override: { - apis: (oI) => { - return { - ...oI, - signUpPOST: async (input) => { - let response = await oI.signUpPOST(input); - if (response.status === "OK") { - customFormFields = input.formFields; - } - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "testValue", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(customFormFields.length === 3); - assert(customFormFields[0].id === "password"); - assert(customFormFields[0].value === "validpass123"); - assert(customFormFields[1].id === "email"); - assert(customFormFields[1].value === "random@gmail.com"); - assert(customFormFields[2].id === "testField"); - assert(customFormFields[2].value === "testValue"); - }); - - //If provided by the user, and no custom fields are there, then the formFields param must sbe empty - it("test that if provided by the user, and no custom fields are there, then formFields must be empty, using handlePostSignUp", async function () { - await startST(); - - let customFormFields = ""; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - signUpPOST: async (input) => { - let response = await oI.signUpPOST(input); - if (response.status === "OK") { - customFormFields = input.formFields; - } - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(customFormFields.length === 2); - }); - - /* formField validation testing: - * - Provide formFields in config but not in input to signup and see error about it being missing - * - Good test case without optional - * - Bad test case without optional (something is missing, and it's not optional) - * - Good test case with optionals - * - Input formFields has no email field (and not in config) - * - Input formFields has no password field (and not in config - * - Input form field has different number of custom fields than in config form fields) - * - Input form field has same number of custom fields as in config form field, but some ids mismatch - * - Test custom field validation error (one and two custom fields) - * - Test password field validation error - * - Test email field validation error - * - Make sure that the input email is trimmed - * - Pass a non string value in the formFields array and make sure it passes through the signUp API and is sent in the handlePostSignup as that type - */ - it("test formFields added in config but not in inout to signup, check error about it being missing", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(response.status === 400); - - assert(JSON.parse(response.text).message === "Are you sending too many / too few formFields?"); - }); - - //- Good test case without optional - it("test valid formFields without optional", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "testValue", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); - }); - - //- Bad test case without optional (something is missing, and it's not optional) - it("test bad case input to signup without optional", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields.length === 1); - assert(response.formFields[0].error === "Field is not optional"); - assert(response.formFields[0].id === "testField"); - }); - - //- Good test case with optionals - it("test good case input to signup with optional", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - optional: true, - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); - }); - - //- Input formFields has no email field (and not in config) - it("test input formFields has no email field", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - ], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Are you sending too many / too few formFields?"); - }); - - // Input formFields has no password field (and not in config - it("test inut formFields has no password field", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Are you sending too many / too few formFields?"); - }); - - // Input form field has different number of custom fields than in config form fields) - it("test input form field has a different number of custom fields than in config form fields", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - optional: true, - }, - { - id: "testField2", - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "", - }, - ], - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Are you sending too many / too few formFields?"); - }); - - // Input form field has same number of custom fields as in config form field, but some ids mismatch - it("test input form field has the same number of custom fields than in config form fields, but ids mismatch", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - optional: true, - }, - { - id: "testField2", - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "", - }, - { - id: "testField3", - value: "", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "FIELD_ERROR"); - assert(response.formFields.length === 1); - assert(response.formFields[0].error === "Field is not optional"); - assert(response.formFields[0].id === "testField2"); - }); - - // Test custom field validation error (one and two custom fields) - it("test custom field validation error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - validate: (value) => { - if (value.length <= 5) { - return "testField validation error"; - } - }, - }, - { - id: "testField2", - validate: (value) => { - if (value.length <= 5) { - return "testField2 validation error"; - } - }, - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: "test", - }, - { - id: "testField2", - value: "test", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields.length === 2); - assert(response.formFields.filter((f) => f.id === "testField")[0].error === "testField validation error"); - assert(response.formFields.filter((f) => f.id === "testField2")[0].error === "testField2 validation error"); - }); - - //Test password field validation error - it("test signup password field validation error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "invalid", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields.length === 1); - assert(response.formFields[0].error === "Password must contain at least 8 characters, including a number"); - assert(response.formFields[0].id === "password"); - }); - - //Test email field validation error - it("test signup email field validation error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "randomgmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields.length === 1); - assert(response.formFields[0].error === "Email is invalid"); - assert(response.formFields[0].id === "email"); - }); - - //Make sure that the input email is trimmed - it("test that input email is trimmed", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: " random@gmail.com ", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - assert(response.user.id !== undefined); - assert(response.user.email === "random@gmail.com"); - }); - - // Pass a non string value in the formFields array and make sure it passes through the signUp API and is sent in the handlePostSignUp as that type - it("test that non string value in formFields array and it passes through the signup API and it is sent to the handlePostSignUp", async function () { - await startST(); - - let customFormFields = ""; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "testField", - }, - ], - }, - override: { - apis: (oI) => { - return { - ...oI, - signUpPOST: async (input) => { - let response = await oI.signUpPOST(input); - if (response.status === "OK") { - customFormFields = input.formFields; - } - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - { - id: "testField", - value: { key: "value" }, - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - assert(customFormFields.length === 3); - assert(customFormFields[2].id === "testField"); - assert(customFormFields[2].value.key === "value"); - }); -}); diff --git a/test/emailpassword/signupFeature.test.ts b/test/emailpassword/signupFeature.test.ts new file mode 100644 index 000000000..d8edb1cac --- /dev/null +++ b/test/emailpassword/signupFeature.test.ts @@ -0,0 +1,1451 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractInfoFromResponse, + killAllST, + printPath, + setupST, + signUPRequest, + startST, +} from '../utils' + +describe(`signupFeature: ${printPath('[test/emailpassword/signupFeature.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // * check if disable api, the default signup API does not work - you get a 404 + it('test that if disable api, the default signup API does not work', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + signUpPOST: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(response.status === 404) + }) + + /* + * test signUpAPI for: + * - it works when the input is fine (sign up, get user id, get email of that user and check the input email is same as the one used for sign up) + * - throws an error in case of duplicate email. + */ + + it('test signUpAPI works when input is fine', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userInfo = JSON.parse(response.text).user + assert(userInfo.id !== undefined) + assert(userInfo.email === 'random@gmail.com') + }) + + it('test signUpAPI throws an error in case of a duplicate email', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userInfo = JSON.parse(response.text).user + assert(userInfo.id !== undefined) + assert(userInfo.email === 'random@gmail.com') + + response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(response.status === 200) + const responseInfo = JSON.parse(response.text) + + assert(responseInfo.status === 'FIELD_ERROR') + assert(responseInfo.formFields.length === 1) + assert(responseInfo.formFields[0].id === 'email') + assert(responseInfo.formFields[0].error === 'This email already exists. Please sign in instead.') + }) + + it('test signUpAPI throws an error for email and password with invalid syntax', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'randomgmail.com', 'invalidpass') + assert(response.status === 200) + const responseInfo = JSON.parse(response.text) + + assert(responseInfo.status === 'FIELD_ERROR') + assert(responseInfo.formFields.length === 2) + assert(responseInfo.formFields.filter(f => f.id === 'email')[0].error === 'Email is invalid') + assert( + responseInfo.formFields.filter(f => f.id === 'password')[0].error + === 'Password must contain at least one number', + ) + }) + + /* pass a bad input to the /signup API and test that it throws a 400 error. + * - Not a JSON + * - No POST body + * - Input is JSON, but wrong structure. + * - formFields is not an array + * - formFields does not exist + * - formField elements have no id or no value field + * */ + it('test bad input, not a JSON to /signup API', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send('hello') + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'Missing input param: formFields') + }) + + it('test bad input, no POST body to /signup API', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signup') + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'Missing input param: formFields') + }) + + it('test bad input, Input is JSON, but wrong structure to /signup API', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + randomKey: 'randomValue', + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'Missing input param: formFields') + }) + + it('test bad input, formFields is not an array in /signup API', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: { + randomKey: 'randomValue', + }, + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'formFields must be an array') + }) + + it('test bad input, formField elements have no id or no value field in /signup API', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const badInputResponse = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + randomKey: 'randomValue', + }, + { + randomKey2: 'randomValue2', + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(badInputResponse.message === 'All elements of formFields must contain an \'id\' and \'value\' field') + }) + + //* Make sure that a successful sign up yields a session + it('test that a successful signup yields a session', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const cookies = extractInfoFromResponse(signUpResponse) + assert(cookies.accessToken !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.antiCsrf !== undefined) + assert(cookies.accessTokenExpiry !== undefined) + assert(cookies.refreshTokenExpiry !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + assert(cookies.frontToken !== undefined) + }) + + /* + * providing the handlePostSignup should work: + * - If not provided by the user, it should not result in an error + * - If provided by the user, and custom fields are there, only those should be sent + * - If provided by the user, and no custom fields are there, then the formFields param must sbe empty + */ + + // If not provided by the user, it should not result in an error + + it('test that if not provided by the user, it should not result in an error', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: 'testValue', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(response.user.id !== undefined) + assert(response.user.email === 'random@gmail.com') + }) + + // - If provided by the user, and custom fields are there, only those should be sent + it('test that if provided by the user, and custom fields are there, only those are sent, using handlePostSignUp', async () => { + await startST() + + let customFormFields = '' + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + }, + ], + }, + override: { + apis: (oI) => { + return { + ...oI, + signUpPOST: async (input) => { + const response = await oI.signUpPOST(input) + if (response.status === 'OK') + customFormFields = input.formFields + + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: 'testValue', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(customFormFields.length === 3) + assert(customFormFields[0].id === 'password') + assert(customFormFields[0].value === 'validpass123') + assert(customFormFields[1].id === 'email') + assert(customFormFields[1].value === 'random@gmail.com') + assert(customFormFields[2].id === 'testField') + assert(customFormFields[2].value === 'testValue') + }) + + // If provided by the user, and no custom fields are there, then the formFields param must sbe empty + it('test that if provided by the user, and no custom fields are there, then formFields must be empty, using handlePostSignUp', async () => { + await startST() + + let customFormFields = '' + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + signUpPOST: async (input) => { + const response = await oI.signUpPOST(input) + if (response.status === 'OK') + customFormFields = input.formFields + + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(customFormFields.length === 2) + }) + + /* formField validation testing: + * - Provide formFields in config but not in input to signup and see error about it being missing + * - Good test case without optional + * - Bad test case without optional (something is missing, and it's not optional) + * - Good test case with optionals + * - Input formFields has no email field (and not in config) + * - Input formFields has no password field (and not in config + * - Input form field has different number of custom fields than in config form fields) + * - Input form field has same number of custom fields as in config form field, but some ids mismatch + * - Test custom field validation error (one and two custom fields) + * - Test password field validation error + * - Test email field validation error + * - Make sure that the input email is trimmed + * - Pass a non string value in the formFields array and make sure it passes through the signUp API and is sent in the handlePostSignup as that type + */ + it('test formFields added in config but not in inout to signup, check error about it being missing', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(response.status === 400) + + assert(JSON.parse(response.text).message === 'Are you sending too many / too few formFields?') + }) + + // - Good test case without optional + it('test valid formFields without optional', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: 'testValue', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(response.user.id !== undefined) + assert(response.user.email === 'random@gmail.com') + }) + + // - Bad test case without optional (something is missing, and it's not optional) + it('test bad case input to signup without optional', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: '', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields.length === 1) + assert(response.formFields[0].error === 'Field is not optional') + assert(response.formFields[0].id === 'testField') + }) + + // - Good test case with optionals + it('test good case input to signup with optional', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + optional: true, + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: '', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(response.user.id !== undefined) + assert(response.user.email === 'random@gmail.com') + }) + + // - Input formFields has no email field (and not in config) + it('test input formFields has no email field', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Are you sending too many / too few formFields?') + }) + + // Input formFields has no password field (and not in config + it('test inut formFields has no password field', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Are you sending too many / too few formFields?') + }) + + // Input form field has different number of custom fields than in config form fields) + it('test input form field has a different number of custom fields than in config form fields', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + optional: true, + }, + { + id: 'testField2', + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: '', + }, + ], + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Are you sending too many / too few formFields?') + }) + + // Input form field has same number of custom fields as in config form field, but some ids mismatch + it('test input form field has the same number of custom fields than in config form fields, but ids mismatch', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + optional: true, + }, + { + id: 'testField2', + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: '', + }, + { + id: 'testField3', + value: '', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'FIELD_ERROR') + assert(response.formFields.length === 1) + assert(response.formFields[0].error === 'Field is not optional') + assert(response.formFields[0].id === 'testField2') + }) + + // Test custom field validation error (one and two custom fields) + it('test custom field validation error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + validate: (value) => { + if (value.length <= 5) + return 'testField validation error' + }, + }, + { + id: 'testField2', + validate: (value) => { + if (value.length <= 5) + return 'testField2 validation error' + }, + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: 'test', + }, + { + id: 'testField2', + value: 'test', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields.length === 2) + assert(response.formFields.filter(f => f.id === 'testField')[0].error === 'testField validation error') + assert(response.formFields.filter(f => f.id === 'testField2')[0].error === 'testField2 validation error') + }) + + // Test password field validation error + it('test signup password field validation error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'invalid', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields.length === 1) + assert(response.formFields[0].error === 'Password must contain at least 8 characters, including a number') + assert(response.formFields[0].id === 'password') + }) + + // Test email field validation error + it('test signup email field validation error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'randomgmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields.length === 1) + assert(response.formFields[0].error === 'Email is invalid') + assert(response.formFields[0].id === 'email') + }) + + // Make sure that the input email is trimmed + it('test that input email is trimmed', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: ' random@gmail.com ', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + assert(response.user.id !== undefined) + assert(response.user.email === 'random@gmail.com') + }) + + // Pass a non string value in the formFields array and make sure it passes through the signUp API and is sent in the handlePostSignUp as that type + it('test that non string value in formFields array and it passes through the signup API and it is sent to the handlePostSignUp', async () => { + await startST() + + let customFormFields = '' + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'testField', + }, + ], + }, + override: { + apis: (oI) => { + return { + ...oI, + signUpPOST: async (input) => { + const response = await oI.signUpPOST(input) + if (response.status === 'OK') + customFormFields = input.formFields + + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + { + id: 'testField', + value: { key: 'value' }, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + assert(customFormFields.length === 3) + assert(customFormFields[2].id === 'testField') + assert(customFormFields[2].value.key === 'value') + }) +}) diff --git a/test/emailpassword/updateEmailPass.test.js b/test/emailpassword/updateEmailPass.test.js deleted file mode 100644 index 227da56c4..000000000 --- a/test/emailpassword/updateEmailPass.test.js +++ /dev/null @@ -1,78 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, signUPRequest } = require("../utils"); -const { updateEmailOrPassword, signIn } = require("../../lib/build/recipe/emailpassword"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -let EmailPassword = require("../../recipe/emailpassword"); -let { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`updateEmailPassTest: ${printPath("[test/emailpassword/updateEmailPass.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test updateEmailPass", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signUPRequest(app, "test@gmail.com", "testPass123"); - - let res = await signIn("test@gmail.com", "testPass123"); - - await updateEmailOrPassword({ - userId: res.user.id, - email: "test2@gmail.com", - password: "testPass", - }); - - let res2 = await signIn("test2@gmail.com", "testPass"); - - assert(res2.user.id === res2.user.id); - }); -}); diff --git a/test/emailpassword/updateEmailPass.test.ts b/test/emailpassword/updateEmailPass.test.ts new file mode 100644 index 000000000..97baf23c2 --- /dev/null +++ b/test/emailpassword/updateEmailPass.test.ts @@ -0,0 +1,78 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import EmailPassword, { signIn, updateEmailOrPassword } from 'supertokens-node/recipe/emailpassword' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { maxVersion } from 'supertokens-node/utils' +import { Querier } from 'supertokens-node/querier' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' + +describe(`updateEmailPassTest: ${printPath('[test/emailpassword/updateEmailPass.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test updateEmailPass', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signUPRequest(app, 'test@gmail.com', 'testPass123') + + const res = await signIn('test@gmail.com', 'testPass123') + + await updateEmailOrPassword({ + userId: res.user.id, + email: 'test2@gmail.com', + password: 'testPass', + }) + + const res2 = await signIn('test2@gmail.com', 'testPass') + + assert(res2.user.id === res2.user.id) + }) +}) diff --git a/test/emailpassword/users.test.js b/test/emailpassword/users.test.js deleted file mode 100644 index f2d3a1ab7..000000000 --- a/test/emailpassword/users.test.js +++ /dev/null @@ -1,281 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, signUPRequest } = require("../utils"); -const { getUserCount, getUsersNewestFirst, getUsersOldestFirst } = require("../../lib/build"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let EmailPassword = require("../../recipe/emailpassword"); -let { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); -const express = require("express"); - -describe(`usersTest: ${printPath("[test/emailpassword/users.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test getUsersOldestFirst", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signUPRequest(app, "test@gmail.com", "testPass123"); - await signUPRequest(app, "test1@gmail.com", "testPass123"); - await signUPRequest(app, "test2@gmail.com", "testPass123"); - await signUPRequest(app, "test3@gmail.com", "testPass123"); - await signUPRequest(app, "test4@gmail.com", "testPass123"); - - let users = await getUsersOldestFirst(); - assert.strictEqual(users.users.length, 5); - assert.strictEqual(users.nextPaginationToken, undefined); - - users = await getUsersOldestFirst({ limit: 1 }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersOldestFirst({ limit: 1, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test1@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersOldestFirst({ limit: 5, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 3); - assert.strictEqual(users.nextPaginationToken, undefined); - - try { - await getUsersOldestFirst({ limit: 10, paginationToken: "invalid-pagination-token" }); - assert(false); - } catch (err) { - if (!err.message.includes("invalid pagination token")) { - throw err; - } - } - - try { - await getUsersOldestFirst({ limit: -1 }); - assert(false); - } catch (err) { - if (!err.message.includes("limit must a positive integer with min value 1")) { - throw err; - } - } - }); - - it("test getUsersOldestFirst with search queries", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - const cdiVersion = await Querier.getNewInstanceOrThrowError("emailpassword").getAPIVersion(); - if (maxVersion("2.20", cdiVersion) !== cdiVersion) { - return; - } - - await signUPRequest(app, "test@gmail.com", "testPass123"); - await signUPRequest(app, "test1@gmail.com", "testPass123"); - await signUPRequest(app, "test2@gmail.com", "testPass123"); - await signUPRequest(app, "test3@gmail.com", "testPass123"); - await signUPRequest(app, "john@gmail.com", "testPass123"); - - let users = await getUsersOldestFirst({ query: { email: "doe" } }); - assert.strictEqual(users.users.length, 0); - - users = await getUsersOldestFirst({ query: { email: "john" } }); - assert.strictEqual(users.users.length, 1); - }); - - it("test getUsersNewestFirst", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signUPRequest(app, "test@gmail.com", "testPass123"); - await signUPRequest(app, "test1@gmail.com", "testPass123"); - await signUPRequest(app, "test2@gmail.com", "testPass123"); - await signUPRequest(app, "test3@gmail.com", "testPass123"); - await signUPRequest(app, "test4@gmail.com", "testPass123"); - - let users = await getUsersNewestFirst(); - assert.strictEqual(users.users.length, 5); - assert.strictEqual(users.nextPaginationToken, undefined); - - users = await getUsersNewestFirst({ limit: 1 }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test4@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersNewestFirst({ limit: 1, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test3@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersNewestFirst({ limit: 5, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 3); - assert.strictEqual(users.nextPaginationToken, undefined); - - try { - await getUsersOldestFirst({ limit: 10, paginationToken: "invalid-pagination-token" }); - assert(false); - } catch (err) { - if (!err.message.includes("invalid pagination token")) { - throw err; - } - } - - try { - await getUsersOldestFirst({ limit: -1 }); - assert(false); - } catch (err) { - if (!err.message.includes("limit must a positive integer with min value 1")) { - throw err; - } - } - }); - - it("test getUsersNewestFirst with search queries", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - const cdiVersion = await Querier.getNewInstanceOrThrowError("emailpassword").getAPIVersion(); - if (maxVersion("2.20", cdiVersion) !== cdiVersion) { - return; - } - - await signUPRequest(app, "test@gmail.com", "testPass123"); - await signUPRequest(app, "test1@gmail.com", "testPass123"); - await signUPRequest(app, "test2@gmail.com", "testPass123"); - await signUPRequest(app, "test3@gmail.com", "testPass123"); - await signUPRequest(app, "john@gmail.com", "testPass123"); - - let users = await getUsersNewestFirst({ query: { email: "doe" } }); - assert.strictEqual(users.users.length, 0); - - users = await getUsersNewestFirst({ query: { email: "john" } }); - assert.strictEqual(users.users.length, 1); - }); - - it("test getUserCount", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let userCount = await getUserCount(); - assert.strictEqual(userCount, 0); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signUPRequest(app, "test@gmail.com", "testPass123"); - userCount = await getUserCount(); - assert.strictEqual(userCount, 1); - - await signUPRequest(app, "test1@gmail.com", "testPass123"); - await signUPRequest(app, "test2@gmail.com", "testPass123"); - await signUPRequest(app, "test3@gmail.com", "testPass123"); - await signUPRequest(app, "test4@gmail.com", "testPass123"); - - userCount = await getUserCount(); - assert.strictEqual(userCount, 5); - }); -}); diff --git a/test/emailpassword/users.test.ts b/test/emailpassword/users.test.ts new file mode 100644 index 000000000..668b2997e --- /dev/null +++ b/test/emailpassword/users.test.ts @@ -0,0 +1,281 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress, { getUserCount, getUsersNewestFirst, getUsersOldestFirst } from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import Session from 'supertokens-node/recipe/session' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' +import express from 'express' + +describe(`usersTest: ${printPath('[test/emailpassword/users.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test getUsersOldestFirst', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signUPRequest(app, 'test@gmail.com', 'testPass123') + await signUPRequest(app, 'test1@gmail.com', 'testPass123') + await signUPRequest(app, 'test2@gmail.com', 'testPass123') + await signUPRequest(app, 'test3@gmail.com', 'testPass123') + await signUPRequest(app, 'test4@gmail.com', 'testPass123') + + let users = await getUsersOldestFirst() + assert.strictEqual(users.users.length, 5) + assert.strictEqual(users.nextPaginationToken, undefined) + + users = await getUsersOldestFirst({ limit: 1 }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersOldestFirst({ limit: 1, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test1@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersOldestFirst({ limit: 5, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 3) + assert.strictEqual(users.nextPaginationToken, undefined) + + try { + await getUsersOldestFirst({ limit: 10, paginationToken: 'invalid-pagination-token' }) + assert(false) + } + catch (err) { + if (!err.message.includes('invalid pagination token')) + throw err + } + + try { + await getUsersOldestFirst({ limit: -1 }) + assert(false) + } + catch (err) { + if (!err.message.includes('limit must a positive integer with min value 1')) + throw err + } + }) + + it("test getUsersOldestFirst with search queries", async function () { + await startST(); + STExpress.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], + }); + + const express = require("express"); + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + const cdiVersion = await Querier.getNewInstanceOrThrowError("emailpassword").getAPIVersion(); + if (maxVersion("2.20", cdiVersion) !== cdiVersion) { + return; + } + + await signUPRequest(app, "test@gmail.com", "testPass123"); + await signUPRequest(app, "test1@gmail.com", "testPass123"); + await signUPRequest(app, "test2@gmail.com", "testPass123"); + await signUPRequest(app, "test3@gmail.com", "testPass123"); + await signUPRequest(app, "john@gmail.com", "testPass123"); + + let users = await getUsersOldestFirst({ query: { email: "doe" } }); + assert.strictEqual(users.users.length, 0); + + users = await getUsersOldestFirst({ query: { email: "john" } }); + assert.strictEqual(users.users.length, 1); + }); + + + it('test getUsersNewestFirst', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signUPRequest(app, 'test@gmail.com', 'testPass123') + await signUPRequest(app, 'test1@gmail.com', 'testPass123') + await signUPRequest(app, 'test2@gmail.com', 'testPass123') + await signUPRequest(app, 'test3@gmail.com', 'testPass123') + await signUPRequest(app, 'test4@gmail.com', 'testPass123') + + let users = await getUsersNewestFirst() + assert.strictEqual(users.users.length, 5) + assert.strictEqual(users.nextPaginationToken, undefined) + + users = await getUsersNewestFirst({ limit: 1 }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test4@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersNewestFirst({ limit: 1, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test3@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersNewestFirst({ limit: 5, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 3) + assert.strictEqual(users.nextPaginationToken, undefined) + + try { + await getUsersOldestFirst({ limit: 10, paginationToken: 'invalid-pagination-token' }) + assert(false) + } + catch (err) { + if (!err.message.includes('invalid pagination token')) + throw err + } + + try { + await getUsersOldestFirst({ limit: -1 }) + assert(false) + } + catch (err) { + if (!err.message.includes('limit must a positive integer with min value 1')) + throw err + } + }) + + it("test getUsersNewestFirst with search queries", async function () { + await startST(); + STExpress.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], + }); + + const express = require("express"); + const app = express(); + + app.use(middleware()); + + app.use(errorHandler()); + + const cdiVersion = await Querier.getNewInstanceOrThrowError("emailpassword").getAPIVersion(); + if (maxVersion("2.20", cdiVersion) !== cdiVersion) { + return; + } + + await signUPRequest(app, "test@gmail.com", "testPass123"); + await signUPRequest(app, "test1@gmail.com", "testPass123"); + await signUPRequest(app, "test2@gmail.com", "testPass123"); + await signUPRequest(app, "test3@gmail.com", "testPass123"); + await signUPRequest(app, "john@gmail.com", "testPass123"); + + let users = await getUsersNewestFirst({ query: { email: "doe" } }); + assert.strictEqual(users.users.length, 0); + + users = await getUsersNewestFirst({ query: { email: "john" } }); + assert.strictEqual(users.users.length, 1); + }); + + it('test getUserCount', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + let userCount = await getUserCount() + assert.strictEqual(userCount, 0) + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signUPRequest(app, 'test@gmail.com', 'testPass123') + userCount = await getUserCount() + assert.strictEqual(userCount, 1) + + await signUPRequest(app, 'test1@gmail.com', 'testPass123') + await signUPRequest(app, 'test2@gmail.com', 'testPass123') + await signUPRequest(app, 'test3@gmail.com', 'testPass123') + await signUPRequest(app, 'test4@gmail.com', 'testPass123') + + userCount = await getUserCount() + assert.strictEqual(userCount, 5) + }) +}) diff --git a/test/error.test.js b/test/error.test.js deleted file mode 100644 index bafb339c1..000000000 --- a/test/error.test.js +++ /dev/null @@ -1,10 +0,0 @@ -const assert = require("assert"); -const { default: SuperTokensError } = require("../lib/build/error"); - -describe("SuperTokensError", () => { - it("should serialize with the proper message", () => { - const err = new SuperTokensError({ type: SuperTokensError.BAD_INPUT_ERROR, message: "test message" }); - - assert.strictEqual(err.toString(), "Error: test message"); - }); -}); diff --git a/test/error.test.ts b/test/error.test.ts new file mode 100644 index 000000000..d350a02cf --- /dev/null +++ b/test/error.test.ts @@ -0,0 +1,11 @@ +import assert from 'assert' +import { describe, it } from 'vitest' +import { SuperTokensError } from 'supertokens-node' + +describe('SuperTokensError', () => { + it('should serialize with the proper message', () => { + const err = new SuperTokensError({ type: SuperTokensError.BAD_INPUT_ERROR, message: 'test message' }) + + assert.strictEqual(err.toString(), 'Error: test message') + }) +}) diff --git a/test/framework/awsLambda.test.js b/test/framework/awsLambda.test.js deleted file mode 100644 index a6fb35686..000000000 --- a/test/framework/awsLambda.test.js +++ /dev/null @@ -1,1190 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - mockLambdaProxyEvent, - mockLambdaProxyEventV2, -} = require("../utils"); -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); -let SuperTokens = require("../../"); -let { middleware } = require("../../framework/awsLambda"); -let Session = require("../../recipe/session"); -let EmailPassword = require("../../recipe/emailpassword"); -let Passwordless = require("../../recipe/passwordless"); -let ThirdParty = require("../../recipe/thirdparty"); -let { Apple, Google, Github } = require("../../recipe/thirdparty"); -let { verifySession } = require("../../recipe/session/framework/awsLambda"); -let Dashboard = require("../../recipe/dashboard"); -let { createUsers } = require("../utils"); -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`AWS Lambda: ${printPath("[test/framework/awsLambda.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - //check basic usage of session - it("test basic usage of sessions for lambda proxy event v1", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiGatewayPath: "/dev", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "userId", {}, {}); - return { - body: JSON.stringify(""), - statusCode: 200, - }; - }; - - let verifyLambdaSession = async (awsEvent, _) => { - return { - body: JSON.stringify({ - user: awsEvent.session.getUserId(), - }), - statusCode: 200, - }; - }; - - let revokeSession = async (awsEvent, _) => { - await awsEvent.session.revokeSession(); - return { - body: JSON.stringify(""), - statusCode: 200, - }; - }; - - let proxy = "/dev"; - let createAccountEvent = mockLambdaProxyEvent("/create", "POST", null, null, proxy); - let result = await middleware(createSession)(createAccountEvent, undefined); - - result.headers = { - ...result.headers, - ...result.multiValueHeaders, - }; - - let res = extractInfoFromResponse(result); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let verifySessionEvent = mockLambdaProxyEvent("/session/verify", "POST", null, null, proxy); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { message: "unauthorised" }); - - verifySessionEvent = mockLambdaProxyEvent( - "/session/verify", - "POST", - { - Cookie: `sAccessToken=${res.accessToken}`, - }, - null, - proxy - ); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { message: "try refresh token" }); - - verifySessionEvent = mockLambdaProxyEvent( - "/session/verify", - "POST", - { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - null, - proxy - ); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { user: "userId" }); - - verifySessionEvent = mockLambdaProxyEvent( - "/session/verify", - "POST", - { - Cookie: `sAccessToken=${res.accessToken}`, - }, - null, - proxy - ); - result = await verifySession(verifyLambdaSession, { - antiCsrfCheck: false, - })(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { user: "userId" }); - - let refreshSessionEvent = mockLambdaProxyEvent("/auth/session/refresh", "POST", null, null, proxy); - result = await middleware()(refreshSessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { message: "unauthorised" }); - - refreshSessionEvent = mockLambdaProxyEvent( - "/auth/session/refresh", - "POST", - { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - null, - proxy - ); - result = await middleware()(refreshSessionEvent, undefined); - result.headers = { - ...result.headers, - ...result.multiValueHeaders, - }; - - let res2 = extractInfoFromResponse(result); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - verifySessionEvent = mockLambdaProxyEvent( - "/session/verify", - "POST", - { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - null, - proxy - ); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { user: "userId" }); - result.headers = { - ...result.headers, - ...result.multiValueHeaders, - }; - let res3 = extractInfoFromResponse(result); - assert(res3.accessToken !== undefined); - - let revokeSessionEvent = mockLambdaProxyEvent( - "/session/revoke", - "POST", - { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - null, - proxy - ); - result = await verifySession(revokeSession)(revokeSessionEvent, undefined); - result.headers = { - ...result.headers, - ...result.multiValueHeaders, - }; - - let sessionRevokedResponseExtracted = extractInfoFromResponse(result); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - //check basic usage of session - it("test basic usage of sessions for lambda proxy event v2", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiGatewayPath: "/dev", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "userId", {}, {}); - return { - body: JSON.stringify(""), - statusCode: 200, - }; - }; - - let verifyLambdaSession = async (awsEvent, _) => { - return { - body: JSON.stringify({ - user: awsEvent.session.getUserId(), - }), - statusCode: 200, - }; - }; - - let revokeSession = async (awsEvent, _) => { - await awsEvent.session.revokeSession(); - return { - body: JSON.stringify(""), - statusCode: 200, - }; - }; - - let proxy = "/dev"; - let createAccountEvent = mockLambdaProxyEventV2("/create", "POST", null, null, proxy, null); - let result = await middleware(createSession)(createAccountEvent, undefined); - - result.headers = { - ...result.headers, - "set-cookie": result.cookies, - }; - - let res = extractInfoFromResponse(result); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let verifySessionEvent = mockLambdaProxyEventV2("/session/verify", "POST", null, null, proxy); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { message: "unauthorised" }); - - verifySessionEvent = mockLambdaProxyEventV2("/session/verify", "POST", null, null, proxy, [ - `sAccessToken=${res.accessToken}`, - ]); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { message: "try refresh token" }); - - verifySessionEvent = mockLambdaProxyEventV2( - "/session/verify", - "POST", - { - "anti-csrf": res.antiCsrf, - }, - null, - proxy, - [`sAccessToken=${res.accessToken}`] - ); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { user: "userId" }); - - verifySessionEvent = mockLambdaProxyEventV2("/session/verify", "POST", null, null, proxy, [ - `sAccessToken=${res.accessToken}`, - ]); - result = await verifySession(verifyLambdaSession, { - antiCsrfCheck: false, - })(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { user: "userId" }); - - let refreshSessionEvent = mockLambdaProxyEventV2("/auth/session/refresh", "POST", null, null, proxy, null); - result = await middleware()(refreshSessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { message: "unauthorised" }); - - refreshSessionEvent = mockLambdaProxyEventV2( - "/auth/session/refresh", - "POST", - { - "anti-csrf": res.antiCsrf, - }, - null, - proxy, - [`sRefreshToken=${res.refreshToken}`] - ); - result = await middleware()(refreshSessionEvent, undefined); - result.headers = { - ...result.headers, - "set-cookie": result.cookies, - }; - - let res2 = extractInfoFromResponse(result); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - verifySessionEvent = mockLambdaProxyEventV2( - "/session/verify", - "POST", - { - "anti-csrf": res2.antiCsrf, - }, - null, - proxy, - [`sAccessToken=${res2.accessToken}`] - ); - result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined); - assert.deepStrictEqual(JSON.parse(result.body), { user: "userId" }); - result.headers = { - ...result.headers, - "set-cookie": result.cookies, - }; - let res3 = extractInfoFromResponse(result); - assert(res3.accessToken !== undefined); - - let revokeSessionEvent = mockLambdaProxyEventV2( - "/session/revoke", - "POST", - { - "anti-csrf": res2.antiCsrf, - }, - null, - proxy, - [`sAccessToken=${res3.accessToken}`] - ); - result = await verifySession(revokeSession)(revokeSessionEvent, undefined); - result.headers = { - ...result.headers, - "set-cookie": result.cookies, - }; - - let sessionRevokedResponseExtracted = extractInfoFromResponse(result); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("sending custom response awslambda", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiGatewayPath: "/dev", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailExistsGET: async function (input) { - input.options.res.setStatusCode(203); - input.options.res.sendJSONResponse({ - custom: true, - }); - return oI.emailExistsGET(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let proxy = "/dev"; - let event = mockLambdaProxyEventV2("/auth/signup/email/exists", "GET", null, null, proxy, null, { - email: "test@example.com", - }); - let result = await middleware()(event, undefined); - assert(result.statusCode === 203); - assert(JSON.parse(result.body).custom); - }); - - for (const tokenTransferMethod of ["header", "cookie"]) { - describe(`Throwing UNATHORISED w/ auth-mode=${tokenTransferMethod}`, () => { - it("should clear all response cookies during refresh", async () => { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: async function (input) { - await oI.refreshPOST(input); - throw new Session.Error({ - message: "unauthorised", - type: Session.Error.UNAUTHORISED, - clearTokens: true, - }); - }, - }; - }, - }, - }), - ], - }); - - let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "userId", {}, {}); - return { - body: JSON.stringify(""), - statusCode: 200, - }; - }; - - let proxy = "/dev"; - let createAccountEvent = mockLambdaProxyEventV2( - "/create", - "POST", - { "st-auth-mode": tokenTransferMethod }, - null, - proxy - ); - let result = await middleware(createSession)(createAccountEvent, undefined); - result.headers = { - ...result.headers, - "set-cookie": result.cookies, - }; - - let res = extractInfoFromResponse(result); - - assert.notStrictEqual(res.accessTokenFromAny, undefined); - assert.notStrictEqual(res.refreshTokenFromAny, undefined); - - const refreshHeaders = - tokenTransferMethod === "header" - ? { authorization: `Bearer ${res.refreshTokenFromAny}` } - : { - cookie: `sRefreshToken=${encodeURIComponent( - res.refreshTokenFromAny - )}; sIdRefreshToken=asdf`, - }; - if (res.antiCsrf) { - refreshHeaders.antiCsrf = res.antiCsrf; - } - - refreshSessionEvent = mockLambdaProxyEventV2( - "/auth/session/refresh", - "POST", - refreshHeaders, - null, - proxy, - null - ); - result = await middleware()(refreshSessionEvent, undefined); - result.headers = { - ...result.headers, - "set-cookie": result.cookies, - }; - - let res2 = extractInfoFromResponse(result); - - assert.strictEqual(res2.status, 401); - if (tokenTransferMethod === "cookie") { - assert.strictEqual(res2.accessToken, ""); - assert.strictEqual(res2.refreshToken, ""); - assert.strictEqual(res2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res2.accessTokenDomain, undefined); - assert.strictEqual(res2.refreshTokenDomain, undefined); - } else { - assert.strictEqual(res2.accessTokenFromHeader, ""); - assert.strictEqual(res2.refreshTokenFromHeader, ""); - } - assert.strictEqual(res2.frontToken, "remove"); - assert.strictEqual(res2.antiCsrf, undefined); - }); - - it("test revoking a session after createNewSession with throwing unauthorised error", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - let createSession = async (awsEvent, _) => { - await Session.createNewSession(awsEvent, awsEvent, "userId", {}, {}); - throw new Session.Error({ - message: "unauthorised", - type: Session.Error.UNAUTHORISED, - clearTokens: true, - }); - }; - - let proxy = "/dev"; - let createAccountEvent = mockLambdaProxyEventV2( - "/create", - "POST", - { "st-auth-mode": tokenTransferMethod }, - null, - proxy - ); - let result = await middleware(createSession)(createAccountEvent, undefined); - result.headers = { - ...result.headers, - "set-cookie": result.cookies, - }; - - let res = extractInfoFromResponse(result); - - assert.strictEqual(res.status, 401); - if (tokenTransferMethod === "cookie") { - assert.strictEqual(res.accessToken, ""); - assert.strictEqual(res.refreshToken, ""); - assert.strictEqual(res.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res.accessTokenDomain, undefined); - assert.strictEqual(res.refreshTokenDomain, undefined); - } else { - assert.strictEqual(res.accessTokenFromHeader, ""); - assert.strictEqual(res.refreshTokenFromHeader, ""); - } - assert.strictEqual(res.frontToken, "remove"); - assert.strictEqual(res.antiCsrf, undefined); - }); - }); - } - - it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - if (authHeader === "Bearer testapikey") { - return true; - } - - return false; - }, - }; - }, - }, - }), - ], - }); - - let proxy = "/dev"; - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users/count", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - null - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - }); - - it("test that tags request respond with correct tags", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - let proxy = "/dev"; - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/search/tags", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - null - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.tags.length !== 0); - }); - - it("test that search results correct output for 'email: t", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - let proxy = "/dev"; - - await createUsers(EmailPassword); - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - { - limit: "10", - email: "t", - } - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.users.length === 5); - }); - - it("test that search results correct output for multiple search items", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - let proxy = "/dev"; - - await createUsers(EmailPassword); - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - { - limit: "10", - email: "john;iresh", - } - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.users.length === 1); - }); - - it("test that search results correct output for 'email: iresh", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - let proxy = "/dev"; - - await createUsers(EmailPassword); - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - { - limit: "10", - email: "iresh", - } - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.users.length === 0); - }); - - it("test that search results correct output for 'phone: +1", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - let proxy = "/dev"; - - await createUsers(null, Passwordless); - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - { - limit: "10", - phone: "+1", - } - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.users.length === 3); - }); - - it("test that search results correct output for 'phone: 1(", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - let proxy = "/dev"; - - await createUsers(null, Passwordless); - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - { - limit: "10", - phone: "1(", - } - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.users.length === 0); - }); - - it("test that search results correct output for 'provider: google'", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - let proxy = "/dev"; - - await createUsers(null, null, ThirdParty); - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - { - limit: "10", - provider: "google", - } - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.users.length === 3); - }); - - it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); - SuperTokens.init({ - framework: "awsLambda", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - let proxy = "/dev"; - - await createUsers(null, null, ThirdParty); - - let event = mockLambdaProxyEventV2( - "/auth/dashboard/api/users", - "GET", - { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - null, - proxy, - null, - { - limit: "10", - provider: "google", - phone: "1", - } - ); - - let result = await middleware()(event, undefined); - assert(result.statusCode === 200); - const body = JSON.parse(result.body); - assert(body.users.length === 0); - }); -}); diff --git a/test/framework/awsLambda.test.ts b/test/framework/awsLambda.test.ts new file mode 100644 index 000000000..85a337817 --- /dev/null +++ b/test/framework/awsLambda.test.ts @@ -0,0 +1,1192 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import { middleware } from 'supertokens-node/framework/awsLambda' +import Session from 'supertokens-node/recipe/session' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import { verifySession } from 'supertokens-node/recipe/session/framework/awsLambda' +import Dashboard from 'supertokens-node/recipe/dashboard' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractInfoFromResponse, + killAllST, + mockLambdaProxyEvent, + mockLambdaProxyEventV2, + printPath, + setupST, + startST, +} from '../utils' + +import { Apple, Github, Google } from 'supertokens-node/recipe/thirdparty' +import { createUsers } from '../utils' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' + +describe(`AWS Lambda: ${printPath('[test/framework/awsLambda.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check basic usage of session + it('test basic usage of sessions for lambda proxy event v1', async () => { + await startST() + SuperTokens.init({ + framework: 'awsLambda', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiGatewayPath: '/dev', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const createSession = async (awsEvent, _) => { + await Session.createNewSession(awsEvent, awsEvent, 'userId', {}, {}) + return { + body: JSON.stringify(''), + statusCode: 200, + } + } + + const verifyLambdaSession = async (awsEvent, _) => { + return { + body: JSON.stringify({ + user: awsEvent.session.getUserId(), + }), + statusCode: 200, + } + } + + const revokeSession = async (awsEvent, _) => { + await awsEvent.session.revokeSession() + return { + body: JSON.stringify(''), + statusCode: 200, + } + } + + const proxy = '/dev' + const createAccountEvent = mockLambdaProxyEvent('/create', 'POST', null, null, proxy) + let result = await middleware(createSession)(createAccountEvent, undefined) + + result.headers = { + ...result.headers, + ...result.multiValueHeaders, + } + + const res = extractInfoFromResponse(result) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + let verifySessionEvent = mockLambdaProxyEvent('/session/verify', 'POST', null, null, proxy) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { message: 'unauthorised' }) + + verifySessionEvent = mockLambdaProxyEvent( + '/session/verify', + 'POST', + { + Cookie: `sAccessToken=${res.accessToken}`, + }, + null, + proxy, + ) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { message: 'try refresh token' }) + + verifySessionEvent = mockLambdaProxyEvent( + '/session/verify', + 'POST', + { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + null, + proxy, + ) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { user: 'userId' }) + + verifySessionEvent = mockLambdaProxyEvent( + '/session/verify', + 'POST', + { + Cookie: `sAccessToken=${res.accessToken}`, + }, + null, + proxy, + ) + result = await verifySession(verifyLambdaSession, { + antiCsrfCheck: false, + })(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { user: 'userId' }) + + let refreshSessionEvent = mockLambdaProxyEvent('/auth/session/refresh', 'POST', null, null, proxy) + result = await middleware()(refreshSessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { message: 'unauthorised' }) + + refreshSessionEvent = mockLambdaProxyEvent( + '/auth/session/refresh', + 'POST', + { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + null, + proxy, + ) + result = await middleware()(refreshSessionEvent, undefined) + result.headers = { + ...result.headers, + ...result.multiValueHeaders, + } + + const res2 = extractInfoFromResponse(result) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + verifySessionEvent = mockLambdaProxyEvent( + '/session/verify', + 'POST', + { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + null, + proxy, + ) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { user: 'userId' }) + result.headers = { + ...result.headers, + ...result.multiValueHeaders, + } + const res3 = extractInfoFromResponse(result) + assert(res3.accessToken !== undefined) + + const revokeSessionEvent = mockLambdaProxyEvent( + '/session/revoke', + 'POST', + { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + null, + proxy, + ) + result = await verifySession(revokeSession)(revokeSessionEvent, undefined) + result.headers = { + ...result.headers, + ...result.multiValueHeaders, + } + + const sessionRevokedResponseExtracted = extractInfoFromResponse(result) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check basic usage of session + it('test basic usage of sessions for lambda proxy event v2', async () => { + await startST() + SuperTokens.init({ + framework: 'awsLambda', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiGatewayPath: '/dev', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const createSession = async (awsEvent, _) => { + await Session.createNewSession(awsEvent, awsEvent, 'userId', {}, {}) + return { + body: JSON.stringify(''), + statusCode: 200, + } + } + + const verifyLambdaSession = async (awsEvent, _) => { + return { + body: JSON.stringify({ + user: awsEvent.session.getUserId(), + }), + statusCode: 200, + } + } + + const revokeSession = async (awsEvent, _) => { + await awsEvent.session.revokeSession() + return { + body: JSON.stringify(''), + statusCode: 200, + } + } + + const proxy = '/dev' + const createAccountEvent = mockLambdaProxyEventV2('/create', 'POST', null, null, proxy, null) + let result = await middleware(createSession)(createAccountEvent, undefined) + + result.headers = { + ...result.headers, + 'set-cookie': result.cookies, + } + + const res = extractInfoFromResponse(result) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + let verifySessionEvent = mockLambdaProxyEventV2('/session/verify', 'POST', null, null, proxy) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { message: 'unauthorised' }) + + verifySessionEvent = mockLambdaProxyEventV2('/session/verify', 'POST', null, null, proxy, [ + `sAccessToken=${res.accessToken}`, + ]) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { message: 'try refresh token' }) + + verifySessionEvent = mockLambdaProxyEventV2( + '/session/verify', + 'POST', + { + 'anti-csrf': res.antiCsrf, + }, + null, + proxy, + [`sAccessToken=${res.accessToken}`], + ) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { user: 'userId' }) + + verifySessionEvent = mockLambdaProxyEventV2('/session/verify', 'POST', null, null, proxy, [ + `sAccessToken=${res.accessToken}`, + ]) + result = await verifySession(verifyLambdaSession, { + antiCsrfCheck: false, + })(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { user: 'userId' }) + + let refreshSessionEvent = mockLambdaProxyEventV2('/auth/session/refresh', 'POST', null, null, proxy, null) + result = await middleware()(refreshSessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { message: 'unauthorised' }) + + refreshSessionEvent = mockLambdaProxyEventV2( + '/auth/session/refresh', + 'POST', + { + 'anti-csrf': res.antiCsrf, + }, + null, + proxy, + [`sRefreshToken=${res.refreshToken}`], + ) + result = await middleware()(refreshSessionEvent, undefined) + result.headers = { + ...result.headers, + 'set-cookie': result.cookies, + } + + const res2 = extractInfoFromResponse(result) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + verifySessionEvent = mockLambdaProxyEventV2( + '/session/verify', + 'POST', + { + 'anti-csrf': res2.antiCsrf, + }, + null, + proxy, + [`sAccessToken=${res2.accessToken}`], + ) + result = await verifySession(verifyLambdaSession)(verifySessionEvent, undefined) + assert.deepStrictEqual(JSON.parse(result.body), { user: 'userId' }) + result.headers = { + ...result.headers, + 'set-cookie': result.cookies, + } + const res3 = extractInfoFromResponse(result) + assert(res3.accessToken !== undefined) + + const revokeSessionEvent = mockLambdaProxyEventV2( + '/session/revoke', + 'POST', + { + 'anti-csrf': res2.antiCsrf, + }, + null, + proxy, + [`sAccessToken=${res3.accessToken}`], + ) + result = await verifySession(revokeSession)(revokeSessionEvent, undefined) + result.headers = { + ...result.headers, + 'set-cookie': result.cookies, + } + + const sessionRevokedResponseExtracted = extractInfoFromResponse(result) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('sending custom response awslambda', async () => { + await startST() + SuperTokens.init({ + framework: 'awsLambda', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiGatewayPath: '/dev', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + async emailExistsGET(input) { + input.options.res.setStatusCode(203) + input.options.res.sendJSONResponse({ + custom: true, + }) + return oI.emailExistsGET(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const proxy = '/dev' + const event = mockLambdaProxyEventV2('/auth/signup/email/exists', 'GET', null, null, proxy, null, { + email: 'test@example.com', + }) + const result = await middleware()(event, undefined) + assert(result.statusCode === 203) + assert(JSON.parse(result.body).custom) + }) + + for (const tokenTransferMethod of ['header', 'cookie']) { + describe(`Throwing UNATHORISED w/ auth-mode=${tokenTransferMethod}`, () => { + it('should clear all response cookies during refresh', async () => { + await startST() + SuperTokens.init({ + framework: 'awsLambda', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + async refreshPOST(input) { + await oI.refreshPOST(input) + throw new Session.Error({ + message: 'unauthorised', + type: Session.Error.UNAUTHORISED, + clearTokens: true, + }) + }, + } + }, + }, + }), + ], + }) + + const createSession = async (awsEvent, _) => { + await Session.createNewSession(awsEvent, awsEvent, 'userId', {}, {}) + return { + body: JSON.stringify(''), + statusCode: 200, + } + } + + const proxy = '/dev' + const createAccountEvent = mockLambdaProxyEventV2( + '/create', + 'POST', + { 'st-auth-mode': tokenTransferMethod }, + null, + proxy, + ) + let result = await middleware(createSession)(createAccountEvent, undefined) + result.headers = { + ...result.headers, + 'set-cookie': result.cookies, + } + + const res = extractInfoFromResponse(result) + + assert.notStrictEqual(res.accessTokenFromAny, undefined) + assert.notStrictEqual(res.refreshTokenFromAny, undefined) + + const refreshHeaders + = tokenTransferMethod === 'header' + ? { authorization: `Bearer ${res.refreshTokenFromAny}` } + : { + cookie: `sRefreshToken=${encodeURIComponent( + res.refreshTokenFromAny, + )}; sIdRefreshToken=asdf`, + } + if (res.antiCsrf) + refreshHeaders.antiCsrf = res.antiCsrf + + const refreshSessionEvent = mockLambdaProxyEventV2( + '/auth/session/refresh', + 'POST', + refreshHeaders, + null, + proxy, + null, + ) + result = await middleware()(refreshSessionEvent, undefined) + result.headers = { + ...result.headers, + 'set-cookie': result.cookies, + } + + const res2 = extractInfoFromResponse(result) + + assert.strictEqual(res2.status, 401) + if (tokenTransferMethod === 'cookie') { + assert.strictEqual(res2.accessToken, '') + assert.strictEqual(res2.refreshToken, '') + assert.strictEqual(res2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res2.accessTokenDomain, undefined) + assert.strictEqual(res2.refreshTokenDomain, undefined) + } + else { + assert.strictEqual(res2.accessTokenFromHeader, '') + assert.strictEqual(res2.refreshTokenFromHeader, '') + } + assert.strictEqual(res2.frontToken, 'remove') + assert.strictEqual(res2.antiCsrf, undefined) + }) + + it('test revoking a session after createNewSession with throwing unauthorised error', async () => { + await startST() + SuperTokens.init({ + framework: 'awsLambda', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const createSession = async (awsEvent, _) => { + await Session.createNewSession(awsEvent, awsEvent, 'userId', {}, {}) + throw new Session.Error({ + message: 'unauthorised', + type: Session.Error.UNAUTHORISED, + clearTokens: true, + }) + } + + const proxy = '/dev' + const createAccountEvent = mockLambdaProxyEventV2( + '/create', + 'POST', + { 'st-auth-mode': tokenTransferMethod }, + null, + proxy, + ) + const result = await middleware(createSession)(createAccountEvent, undefined) + result.headers = { + ...result.headers, + 'set-cookie': result.cookies, + } + + const res = extractInfoFromResponse(result) + + assert.strictEqual(res.status, 401) + if (tokenTransferMethod === 'cookie') { + assert.strictEqual(res.accessToken, '') + assert.strictEqual(res.refreshToken, '') + assert.strictEqual(res.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res.accessTokenDomain, undefined) + assert.strictEqual(res.refreshTokenDomain, undefined) + } + else { + assert.strictEqual(res.accessTokenFromHeader, '') + assert.strictEqual(res.refreshTokenFromHeader, '') + } + assert.strictEqual(res.frontToken, 'remove') + assert.strictEqual(res.antiCsrf, undefined) + }) + }) + } + + it('test that authorization header is read correctly in dashboard recipe', async () => { + await startST() + SuperTokens.init({ + framework: 'awsLambda', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [ + Dashboard.init({ + apiKey: 'testapikey', + override: { + functions: (original) => { + return { + ...original, + async shouldAllowAccess(input) { + const authHeader = input.req.getHeaderValue('authorization') + if (authHeader === 'Bearer testapikey') + return true + + return false + }, + } + }, + }, + }), + ], + }) + + const proxy = '/dev' + + const event = mockLambdaProxyEventV2( + '/auth/dashboard/api/users/count', + 'GET', + { + 'Authorization': 'Bearer testapikey', + 'Content-Type': 'application/json', + }, + null, + proxy, + null, + null, + ) + + const result = await middleware()(event, undefined) + assert(result.statusCode === 200) + }) + + + it("test that tags request respond with correct tags", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + let proxy = "/dev"; + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/search/tags", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + null + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.tags.length !== 0); + }); + + it("test that search results correct output for 'email: t", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + let proxy = "/dev"; + + await createUsers(EmailPassword); + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/users", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + { + limit: "10", + email: "t", + } + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.users.length === 5); + }); + + it("test that search results correct output for multiple search items", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + let proxy = "/dev"; + + await createUsers(EmailPassword); + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/users", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + { + limit: "10", + email: "john;iresh", + } + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.users.length === 1); + }); + + it("test that search results correct output for 'email: iresh", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + let proxy = "/dev"; + + await createUsers(EmailPassword); + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/users", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + { + limit: "10", + email: "iresh", + } + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.users.length === 0); + }); + + it("test that search results correct output for 'phone: +1", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + let proxy = "/dev"; + + await createUsers(null, Passwordless); + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/users", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + { + limit: "10", + phone: "+1", + } + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.users.length === 3); + }); + + it("test that search results correct output for 'phone: 1(", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + let proxy = "/dev"; + + await createUsers(null, Passwordless); + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/users", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + { + limit: "10", + phone: "1(", + } + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.users.length === 0); + }); + + it("test that search results correct output for 'provider: google'", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + let proxy = "/dev"; + + await createUsers(null, null, ThirdParty); + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/users", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + { + limit: "10", + provider: "google", + } + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.users.length === 3); + }); + + it("test that search results correct output for 'provider: google, phone: 1'", async function () { + await startST(); + SuperTokens.init({ + framework: "awsLambda", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "http://api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "http://supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + let proxy = "/dev"; + + await createUsers(null, null, ThirdParty); + + let event = mockLambdaProxyEventV2( + "/auth/dashboard/api/users", + "GET", + { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + null, + proxy, + null, + { + limit: "10", + provider: "google", + phone: "1", + } + ); + + let result = await middleware()(event, undefined); + assert(result.statusCode === 200); + const body = JSON.parse(result.body); + assert(body.users.length === 0); + }); +}) diff --git a/test/framework/fastify.test.js b/test/framework/fastify.test.js deleted file mode 100644 index bd61f6c2b..000000000 --- a/test/framework/fastify.test.js +++ /dev/null @@ -1,1884 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - extractCookieCountInfo, -} = require("../utils"); -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); -let SuperTokens = require("../../"); -let FastifyFramework = require("../../framework/fastify"); -const Fastify = require("fastify"); -let EmailPassword = require("../../recipe/emailpassword"); -const EmailVerification = require("../../recipe/emailverification"); -let Session = require("../../recipe/session"); -let { verifySession } = require("../../recipe/session/framework/fastify"); -let Dashboard = require("../../recipe/dashboard"); -let { createUsers } = require("../utils"); -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const Passwordless = require("../../recipe/passwordless"); -const ThirdParty = require("../../recipe/thirdparty"); -const { Apple, Google, Github } = require("../../recipe/thirdparty"); - -describe(`Fastify: ${printPath("[test/framework/fastify.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - this.server = Fastify(); - }); - - afterEach(async function () { - try { - await this.server.close(); - } catch (err) {} - }); - after(async function () { - await killAllST(); - await cleanST(); - }); - - // check if disabling api, the default refresh API does not work - you get a 404 - it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - assert(res2.statusCode === 404); - }); - - it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - await this.server.inject({ - method: "post", - url: "/create", - }); - - let res2 = await this.server.inject({ - method: "post", - url: "/auth/signout", - }); - - assert(res2.statusCode === 404); - }); - - //- check for token theft detection - it("token theft detection", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { - response.sendJSONResponse({ - success: true, - }); - }, - }, - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - }), - ], - }); - - this.server.setErrorHandler(FastifyFramework.errorHandler()); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - this.server.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - return res.send("").code(200); - }); - - this.server.post("/auth/session/refresh", async (req, res) => { - await Session.refreshSession(req, res); - return res.send({ success: false }).code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - - let res3 = await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert.strictEqual(res3.json().success, true); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - }); - - // - check for token theft detection - it("token theft detection with auto refresh middleware", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.setErrorHandler(FastifyFramework.errorHandler()); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - this.server.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - return res.send("").code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - - let res3 = await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert(res3.statusCode === 401); - assert.deepStrictEqual(res3.json(), { message: "token theft detected" }); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - }); - - // - check for token theft detection without error handler - it("token theft detection with auto refresh middleware without error handler", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - this.server.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - return res.send("").code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - - let res3 = await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert(res3.statusCode === 401); - assert.deepStrictEqual(res3.json(), { message: "token theft detected" }); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - }); - - // - check if session verify middleware responds with a nice error even without the global error handler - it("test session verify middleware without error handler added", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post( - "/session/verify", - { - preHandler: verifySession(), - }, - async (req, res) => { - return res.send("").code(200); - } - ); - - await this.server.register(FastifyFramework.plugin); - - let res = await this.server.inject({ - method: "post", - url: "/session/verify", - }); - - assert.strictEqual(res.statusCode, 401); - assert.deepStrictEqual(res.json(), { message: "unauthorised" }); - }); - - // check basic usage of session - it("test basic usage of sessions", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - this.server.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - return res.send("").code(200); - }); - - this.server.post("/session/revoke", async (req, res) => { - let session = await Session.getSession(req, res); - await session.revokeSession(); - return res.send("").code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/session/revoke", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test signout API works", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/auth/signout", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - // check basic usage of session - it("test basic usage of sessions with auto refresh", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - this.server.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - return res.send("").code(200); - }); - - this.server.post("/session/revoke", async (req, res) => { - let session = await Session.getSession(req, res); - await session.revokeSession(); - return res.send("").code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/session/revoke", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - // check session verify for with / without anti-csrf present - it("test session verify with anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - return res.send("").code(200); - }); - - this.server.post("/session/verify", async (req, res) => { - let sessionResponse = await Session.getSession(req, res); - return res.send({ userId: sessionResponse.userId }).code(200); - }); - - this.server.post("/session/verifyAntiCsrfFalse", async (req, res) => { - let sessionResponse = await Session.getSession(req, res, false); - return res.send({ userId: sessionResponse.userId }).code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert.strictEqual(res2.json().userId, "id1"); - - let res3 = await this.server.inject({ - method: "post", - url: "/session/verifyAntiCsrfFalse", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert.strictEqual(res3.json().userId, "id1"); - }); - - // check session verify for with / without anti-csrf present - it("test session verify without anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - await this.server.register(FastifyFramework.plugin); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - return res.send("").code(200); - }); - - this.server.post("/session/verify", async (req, res) => { - try { - await Session.getSession(req, res, { antiCsrfCheck: true }); - return res.send({ success: false }).code(200); - } catch (err) { - return res - .send({ - success: err.type === Session.Error.TRY_REFRESH_TOKEN, - }) - .code(200); - } - }); - - this.server.post("/session/verifyAntiCsrfFalse", async (req, res) => { - let sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: false }); - return res.send({ userId: sessionResponse.userId }).code(200); - }); - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let response2 = await this.server.inject({ - method: "post", - url: "/session/verifyAntiCsrfFalse", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - }, - }); - assert.strictEqual(response2.json().userId, "id1"); - - let response = await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - }, - }); - assert.strictEqual(response.json().success, true); - }); - - //check revoking session(s)** - it("test revoking sessions", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - this.server.post("/usercreate", async (req, res) => { - await Session.createNewSession(req, res, "someUniqueUserId", {}, {}); - return res.send("").code(200); - }); - this.server.post("/session/revoke", async (req, res) => { - let session = await Session.getSession(req, res); - await session.revokeSession(); - return res.send("").code(200); - }); - - this.server.post("/session/revokeUserid", async (req, res) => { - let session = await Session.getSession(req, res); - await Session.revokeAllSessionsForUser(session.getUserId()); - return res.send("").code(200); - }); - - this.server.post("/session/getSessionsWithUserId1", async (req, res) => { - let sessionHandles = await Session.getAllSessionHandlesForUser("someUniqueUserId"); - return res.send(sessionHandles).code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/session/revoke", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - - await this.server.inject({ - method: "post", - url: "/usercreate", - }); - let userCreateResponse = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/usercreate", - }) - ); - - await this.server.inject({ - method: "post", - url: "/session/revokeUserid", - headers: { - Cookie: `sAccessToken=${userCreateResponse.accessToken}`, - "anti-csrf": userCreateResponse.antiCsrf, - }, - }); - let sessionHandleResponse = await this.server.inject({ - method: "post", - url: "/session/getSessionsWithUserId1", - }); - assert(sessionHandleResponse.json().length === 0); - }); - - //check manipulating session data - it("test manipulating session data", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.send("").code(200); - }); - - this.server.post( - "/updateSessionData", - { - preHandler: verifySession(), - }, - async (req, res) => { - await req.session.updateSessionData({ key: "value" }); - return res.send("").code(200); - } - ); - - this.server.post( - "/getSessionData", - { - preHandler: verifySession(), - }, - async (req, res) => { - let sessionData = await req.session.getSessionData(); - return res.send(sessionData).code(200); - } - ); - - this.server.post( - "/updateSessionData2", - { - preHandler: verifySession(), - }, - async (req, res) => { - await req.session.updateSessionData(null); - return res.send("").code(200); - } - ); - - this.server.post("/updateSessionDataInvalidSessionHandle", async (req, res) => { - return res - .send({ success: !(await Session.updateSessionData("InvalidHandle", { key: "value3" })) }) - .code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - //create a new session - let response = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - await this.server.inject({ - method: "post", - url: "/updateSessionData", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //call the getSessionData api to get session data - let response2 = await this.server.inject({ - method: "post", - url: "/getSessionData", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //check that the session data returned is valid - assert.strictEqual(response2.json().key, "value"); - - // change the value of the inserted session data - await this.server.inject({ - method: "post", - url: "/updateSessionData2", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //retrieve the changed session data - response2 = await this.server.inject({ - method: "post", - url: "/getSessionData", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //check the value of the retrieved - assert.deepStrictEqual(response2.json(), {}); - - //invalid session handle when updating the session data - let invalidSessionResponse = await this.server.inject({ - method: "post", - url: "/updateSessionDataInvalidSessionHandle", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - assert.strictEqual(invalidSessionResponse.json().success, true); - }); - - //check manipulating jwt payload - it("test manipulating jwt payload", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "user1", {}, {}); - return res.send("").code(200); - }); - - this.server.post( - "/updateAccessTokenPayload", - { - preHandler: verifySession(), - }, - async (req, res) => { - let accessTokenBefore = req.session.accessToken; - await req.session.updateAccessTokenPayload({ key: "value" }); - let accessTokenAfter = req.session.accessToken; - let statusCode = - accessTokenBefore !== accessTokenAfter && typeof accessTokenAfter === "string" ? 200 : 500; - return res.send("").code(statusCode); - } - ); - - this.server.post( - "/getAccessTokenPayload", - { - preHandler: verifySession(), - }, - async (req, res) => { - let jwtPayload = await req.session.getAccessTokenPayload(); - return res.send(jwtPayload).code(200); - } - ); - - this.server.post( - "/updateAccessTokenPayload2", - { - preHandler: verifySession(), - }, - async (req, res) => { - await req.session.updateAccessTokenPayload(null); - return res.send("").code(200); - } - ); - - this.server.post("/updateAccessTokenPayloadInvalidSessionHandle", async (req, res) => { - return res - .send({ success: !(await Session.updateSessionData("InvalidHandle", { key: "value3" })) }) - .code(200); - }); - - await this.server.register(FastifyFramework.plugin); - - //create a new session - let response = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //call the updateAccessTokenPayload api to add jwt payload - let updatedResponse = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/updateAccessTokenPayload", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - //call the getAccessTokenPayload api to get jwt payload - let response2 = await this.server.inject({ - method: "post", - url: "/getAccessTokenPayload", - headers: { - Cookie: `sAccessToken=${updatedResponse.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - //check that the jwt payload returned is valid - assert.strictEqual(response2.json().key, "value"); - - // refresh session - response2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${response.refreshToken}`, - "anti-csrf": response.antiCsrf, - }, - }) - ); - - frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - // change the value of the inserted jwt payload - let updatedResponse2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/updateAccessTokenPayload2", - headers: { - Cookie: `sAccessToken=${response2.accessToken}`, - "anti-csrf": response2.antiCsrf, - }, - }) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //retrieve the changed jwt payload - let response3 = await this.server.inject({ - method: "post", - url: "/getAccessTokenPayload", - headers: { - Cookie: `sAccessToken=${updatedResponse2.accessToken}`, - "anti-csrf": response2.antiCsrf, - }, - }); - - //check the value of the retrieved - assert.deepStrictEqual(response3.json(), {}); - //invalid session handle when updating the jwt payload - let invalidSessionResponse = await this.server.inject({ - method: "post", - url: "/updateAccessTokenPayloadInvalidSessionHandle", - headers: { - Cookie: `sAccessToken=${updatedResponse2.accessToken}`, - "anti-csrf": response2.antiCsrf, - }, - }); - assert.strictEqual(invalidSessionResponse.json().success, true); - }); - - it("sending custom response fastify", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailExistsGET: async function (input) { - input.options.res.setStatusCode(203); - input.options.res.sendJSONResponse({ - custom: true, - }); - return oI.emailExistsGET(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - await this.server.register(FastifyFramework.plugin); - - //create a new session - let response = await this.server.inject({ - method: "get", - url: "/auth/signup/email/exists?email=test@example.com", - }); - - assert(response.statusCode === 203); - - assert(JSON.parse(response.body).custom); - }); - - it("generating email verification token without payload", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - EmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - await this.server.register(FastifyFramework.plugin); - - // sign up a user first - let response = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/signup", - payload: { - formFields: [ - { - id: "email", - value: "johndoe@gmail.com", - }, - { - id: "password", - value: "testPass123", - }, - ], - }, - }) - ); - - // send generate email verification token request - let res2 = await this.server.inject({ - method: "post", - url: "/auth/user/email/verify/token", - payload: {}, - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "Content-Type": "application/json", - }, - }); - - assert.equal(res2.statusCode, 200); - }); - - it("test same cookie is not getting set multiple times", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - await this.server.register(FastifyFramework.plugin); - - this.server.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - return res.send("").code(200); - }); - - let res = extractCookieCountInfo( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - assert.strictEqual(res.accessToken, 1); - assert.strictEqual(res.refreshToken, 1); - }); - - it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - if (authHeader === "Bearer testapikey") { - return true; - } - - return false; - }, - }; - }, - }, - }), - ], - }); - await this.server.register(FastifyFramework.plugin); - - let res2 = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users/count", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(res2.statusCode === 200); - }); - - it("test that tags request respond with correct tags", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(FastifyFramework.plugin); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/search/tags", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.tags.length !== 0); - }); - - it("test that search results correct output for 'email: t'", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(FastifyFramework.plugin); - await createUsers(EmailPassword); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&email=t", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.users.length === 5); - }); - - it("test that search results correct output for multiple search terms", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(FastifyFramework.plugin); - await createUsers(EmailPassword); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&email=iresh;john", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.users.length === 1); - }); - - it("test that search results correct output for 'email: iresh'", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(FastifyFramework.plugin); - await createUsers(EmailPassword); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&email=iresh", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.users.length === 0); - }); - - it("test that search results correct output for 'phone: +1'", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await createUsers(null, Passwordless); - await this.server.register(FastifyFramework.plugin); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&phone=%2B1", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.users.length === 3); - }); - - it("test that search results correct output for 'phone: 1('", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await createUsers(null, null); - await this.server.register(FastifyFramework.plugin); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&phone=1%28", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.users.length === 0); - }); - - it("test that search results correct output for 'provider: google'", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await createUsers(null, null, ThirdParty); - await this.server.register(FastifyFramework.plugin); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&provider=google", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.users.length === 3); - }); - - it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); - SuperTokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await createUsers(null, Passwordless, ThirdParty); - await this.server.register(FastifyFramework.plugin); - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&provider=google&phone=1", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(resp.statusCode === 200); - const body = resp.json(); - assert(body.users.length === 0); - }); -}); diff --git a/test/framework/fastify.test.ts b/test/framework/fastify.test.ts new file mode 100644 index 000000000..c8e72f739 --- /dev/null +++ b/test/framework/fastify.test.ts @@ -0,0 +1,1891 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import * as FastifyFramework from 'supertokens-node/framework/fastify' +import Fastify, { FastifyInstance } from 'fastify' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import Session from 'supertokens-node/recipe/session' +import { verifySession } from 'supertokens-node/recipe/session/framework/fastify' +import Dashboard from 'supertokens-node/recipe/dashboard' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractCookieCountInfo, + extractInfoFromResponse, + killAllST, + printPath, + setupST, + startST, +} from '../utils' + +import { Apple, Github, Google } from 'supertokens-node/recipe/thirdparty' +import { createUsers } from '../utils' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' + +import Passwordless from 'supertokens-node/recipe/passwordless' + + +describe(`Fastify: ${printPath('[test/framework/fastify.test.ts]')}`, () => { + let server: FastifyInstance + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + server = Fastify() + }) + + afterEach(async () => { + try { + await server.close() + } + catch (err) {} + }) + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check if disabling api, the default refresh API does not work - you get a 404 + it('test that if disabling api, the default refresh API does not work', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + assert(res2.statusCode === 404) + }) + + it('test that if disabling api, the default sign out API does not work', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + await server.register(FastifyFramework.plugin) + + await server.inject({ + method: 'post', + url: '/create', + }) + + const res2 = await server.inject({ + method: 'post', + url: '/auth/signout', + }) + + assert(res2.statusCode === 404) + }) + + // - check for token theft detection + it('token theft detection', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + errorHandlers: { + onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + response.sendJSONResponse({ + success: true, + }) + }, + }, + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + }), + ], + }) + + server.setErrorHandler(FastifyFramework.errorHandler()) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + server.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + return res.send('').code(200) + }) + + server.post('/auth/session/refresh', async (req, res) => { + await Session.refreshSession(req, res) + return res.send({ success: false }).code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + + const res3 = await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert.strictEqual(res3.json().success, true) + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + }) + + // - check for token theft detection + it('token theft detection with auto refresh middleware', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.setErrorHandler(FastifyFramework.errorHandler()) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + server.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + return res.send('').code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + + const res3 = await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert(res3.statusCode === 401) + assert.deepStrictEqual(res3.json(), { message: 'token theft detected' }) + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + }) + + // - check for token theft detection without error handler + it('token theft detection with auto refresh middleware without error handler', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + server.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + return res.send('').code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + + const res3 = await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert(res3.statusCode === 401) + assert.deepStrictEqual(res3.json(), { message: 'token theft detected' }) + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + }) + + // - check if session verify middleware responds with a nice error even without the global error handler + it('test session verify middleware without error handler added', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post( + '/session/verify', + { + preHandler: verifySession(), + }, + async (req, res) => { + return res.send('').code(200) + }, + ) + + await server.register(FastifyFramework.plugin) + + const res = await server.inject({ + method: 'post', + url: '/session/verify', + }) + + assert.strictEqual(res.statusCode, 401) + assert.deepStrictEqual(res.json(), { message: 'unauthorised' }) + }) + + // check basic usage of session + it('test basic usage of sessions', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + server.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + return res.send('').code(200) + }) + + server.post('/session/revoke', async (req, res) => { + const session = await Session.getSession(req, res) + await session.revokeSession() + return res.send('').code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/session/revoke', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test signout API works', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/auth/signout', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check basic usage of session + it('test basic usage of sessions with auto refresh', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + server.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + return res.send('').code(200) + }) + + server.post('/session/revoke', async (req, res) => { + const session = await Session.getSession(req, res) + await session.revokeSession() + return res.send('').code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/session/revoke', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check session verify for with / without anti-csrf present + it('test session verify with anti-csrf present', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + return res.send('').code(200) + }) + + server.post('/session/verify', async (req, res) => { + const sessionResponse = await Session.getSession(req, res) + return res.send({ userId: sessionResponse.userId }).code(200) + }) + + server.post('/session/verifyAntiCsrfFalse', async (req, res) => { + const sessionResponse = await Session.getSession(req, res, false) + return res.send({ userId: sessionResponse.userId }).code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert.strictEqual(res2.json().userId, 'id1') + + const res3 = await server.inject({ + method: 'post', + url: '/session/verifyAntiCsrfFalse', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert.strictEqual(res3.json().userId, 'id1') + }) + + // check session verify for with / without anti-csrf present + it('test session verify without anti-csrf present', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + await server.register(FastifyFramework.plugin) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + return res.send('').code(200) + }) + + server.post('/session/verify', async (req, res) => { + try { + await Session.getSession(req, res, { antiCsrfCheck: true }) + return res.send({ success: false }).code(200) + } + catch (err) { + return res + .send({ + success: err.type === Session.Error.TRY_REFRESH_TOKEN, + }) + .code(200) + } + }) + + server.post('/session/verifyAntiCsrfFalse', async (req, res) => { + const sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: false }) + return res.send({ userId: sessionResponse.userId }).code(200) + }) + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const response2 = await server.inject({ + method: 'post', + url: '/session/verifyAntiCsrfFalse', + headers: { + Cookie: `sAccessToken=${res.accessToken}`, + }, + }) + assert.strictEqual(response2.json().userId, 'id1') + + const response = await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + Cookie: `sAccessToken=${res.accessToken}`, + }, + }) + assert.strictEqual(response.json().success, true) + }) + + // check revoking session(s)** + it('test revoking sessions', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + server.post('/usercreate', async (req, res) => { + await Session.createNewSession(req, res, 'someUniqueUserId', {}, {}) + return res.send('').code(200) + }) + server.post('/session/revoke', async (req, res) => { + const session = await Session.getSession(req, res) + await session.revokeSession() + return res.send('').code(200) + }) + + server.post('/session/revokeUserid', async (req, res) => { + const session = await Session.getSession(req, res) + await Session.revokeAllSessionsForUser(session.getUserId()) + return res.send('').code(200) + }) + + server.post('/session/getSessionsWithUserId1', async (req, res) => { + const sessionHandles = await Session.getAllSessionHandlesForUser('someUniqueUserId') + return res.send(sessionHandles).code(200) + }) + + await server.register(FastifyFramework.plugin) + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/session/revoke', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + + await server.inject({ + method: 'post', + url: '/usercreate', + }) + const userCreateResponse = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/usercreate', + }), + ) + + await server.inject({ + method: 'post', + url: '/session/revokeUserid', + headers: { + 'Cookie': `sAccessToken=${userCreateResponse.accessToken}`, + 'anti-csrf': userCreateResponse.antiCsrf, + }, + }) + const sessionHandleResponse = await server.inject({ + method: 'post', + url: '/session/getSessionsWithUserId1', + }) + assert(sessionHandleResponse.json().length === 0) + }) + + // check manipulating session data + it('test manipulating session data', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.send('').code(200) + }) + + server.post( + '/updateSessionData', + { + preHandler: verifySession(), + }, + async (req, res) => { + await req.session.updateSessionData({ key: 'value' }) + return res.send('').code(200) + }, + ) + + server.post( + '/getSessionData', + { + preHandler: verifySession(), + }, + async (req, res) => { + const sessionData = await req.session.getSessionData() + return res.send(sessionData).code(200) + }, + ) + + server.post( + '/updateSessionData2', + { + preHandler: verifySession(), + }, + async (req, res) => { + await req.session.updateSessionData(null) + return res.send('').code(200) + }, + ) + + server.post('/updateSessionDataInvalidSessionHandle', async (req, res) => { + return res + .send({ success: !(await Session.updateSessionData('InvalidHandle', { key: 'value3' })) }) + .code(200) + }) + + await server.register(FastifyFramework.plugin) + + // create a new session + const response = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + await server.inject({ + method: 'post', + url: '/updateSessionData', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // call the getSessionData api to get session data + let response2 = await server.inject({ + method: 'post', + url: '/getSessionData', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // check that the session data returned is valid + assert.strictEqual(response2.json().key, 'value') + + // change the value of the inserted session data + await server.inject({ + method: 'post', + url: '/updateSessionData2', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // retrieve the changed session data + response2 = await server.inject({ + method: 'post', + url: '/getSessionData', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // check the value of the retrieved + assert.deepStrictEqual(response2.json(), {}) + + // invalid session handle when updating the session data + const invalidSessionResponse = await server.inject({ + method: 'post', + url: '/updateSessionDataInvalidSessionHandle', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + assert.strictEqual(invalidSessionResponse.json().success, true) + }) + + // check manipulating jwt payload + it('test manipulating jwt payload', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'user1', {}, {}) + return res.send('').code(200) + }) + + server.post( + '/updateAccessTokenPayload', + { + preHandler: verifySession(), + }, + async (req, res) => { + const accessTokenBefore = req.session.accessToken + await req.session.updateAccessTokenPayload({ key: 'value' }) + const accessTokenAfter = req.session.accessToken + const statusCode + = accessTokenBefore !== accessTokenAfter && typeof accessTokenAfter === 'string' ? 200 : 500 + return res.send('').code(statusCode) + }, + ) + + server.post( + '/getAccessTokenPayload', + { + preHandler: verifySession(), + }, + async (req, res) => { + const jwtPayload = await req.session.getAccessTokenPayload() + return res.send(jwtPayload).code(200) + }, + ) + + server.post( + '/updateAccessTokenPayload2', + { + preHandler: verifySession(), + }, + async (req, res) => { + await req.session.updateAccessTokenPayload(null) + return res.send('').code(200) + }, + ) + + server.post('/updateAccessTokenPayloadInvalidSessionHandle', async (req, res) => { + return res + .send({ success: !(await Session.updateSessionData('InvalidHandle', { key: 'value3' })) }) + .code(200) + }) + + await server.register(FastifyFramework.plugin) + + // create a new session + const response = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // call the updateAccessTokenPayload api to add jwt payload + const updatedResponse = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/updateAccessTokenPayload', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + // call the getAccessTokenPayload api to get jwt payload + let response2 = await server.inject({ + method: 'post', + url: '/getAccessTokenPayload', + headers: { + 'Cookie': `sAccessToken=${updatedResponse.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + // check that the jwt payload returned is valid + assert.strictEqual(response2.json().key, 'value') + + // refresh session + response2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${response.refreshToken}`, + 'anti-csrf': response.antiCsrf, + }, + }), + ) + + frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + // change the value of the inserted jwt payload + const updatedResponse2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/updateAccessTokenPayload2', + headers: { + 'Cookie': `sAccessToken=${response2.accessToken}`, + 'anti-csrf': response2.antiCsrf, + }, + }), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // retrieve the changed jwt payload + const response3 = await server.inject({ + method: 'post', + url: '/getAccessTokenPayload', + headers: { + 'Cookie': `sAccessToken=${updatedResponse2.accessToken}`, + 'anti-csrf': response2.antiCsrf, + }, + }) + + // check the value of the retrieved + assert.deepStrictEqual(response3.json(), {}) + // invalid session handle when updating the jwt payload + const invalidSessionResponse = await server.inject({ + method: 'post', + url: '/updateAccessTokenPayloadInvalidSessionHandle', + headers: { + 'Cookie': `sAccessToken=${updatedResponse2.accessToken}`, + 'anti-csrf': response2.antiCsrf, + }, + }) + assert.strictEqual(invalidSessionResponse.json().success, true) + }) + + it('sending custom response fastify', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + async emailExistsGET(input) { + input.options.res.setStatusCode(203) + input.options.res.sendJSONResponse({ + custom: true, + }) + return oI.emailExistsGET(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + await server.register(FastifyFramework.plugin) + + // create a new session + const response = await server.inject({ + method: 'get', + url: '/auth/signup/email/exists?email=test@example.com', + }) + + assert(response.statusCode === 203) + + assert(JSON.parse(response.body).custom) + }) + + it('generating email verification token without payload', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + EmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + await server.register(FastifyFramework.plugin) + + // sign up a user first + const response = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/signup', + payload: { + formFields: [ + { + id: 'email', + value: 'johndoe@gmail.com', + }, + { + id: 'password', + value: 'testPass123', + }, + ], + }, + }), + ) + + // send generate email verification token request + const res2 = await server.inject({ + method: 'post', + url: '/auth/user/email/verify/token', + payload: {}, + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + assert.equal(res2.statusCode, 200) + }) + + it('test same cookie is not getting set multiple times', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + await server.register(FastifyFramework.plugin) + + server.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + return res.send('').code(200) + }) + + const res = extractCookieCountInfo( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + assert.strictEqual(res.accessToken, 1) + assert.strictEqual(res.refreshToken, 1) + }) + + it('test that authorization header is read correctly in dashboard recipe', async () => { + await startST() + SuperTokens.init({ + framework: 'fastify', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Dashboard.init({ + apiKey: 'testapikey', + override: { + functions: (original) => { + return { + ...original, + async shouldAllowAccess(input) { + const authHeader = input.req.getHeaderValue('authorization') + if (authHeader === 'Bearer testapikey') + return true + + return false + }, + } + }, + }, + }), + ], + }) + await server.register(FastifyFramework.plugin) + + const res2 = await server.inject({ + method: 'get', + url: '/auth/dashboard/api/users/count', + headers: { + 'Authorization': 'Bearer testapikey', + 'Content-Type': 'application/json', + }, + }) + + assert(res2.statusCode === 200) + }) + + + it("test that tags request respond with correct tags", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(FastifyFramework.plugin); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/search/tags", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.tags.length !== 0); + }); + + it("test that search results correct output for 'email: t'", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(FastifyFramework.plugin); + await createUsers(EmailPassword); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&email=t", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.users.length === 5); + }); + + it("test that search results correct output for multiple search terms", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(FastifyFramework.plugin); + await createUsers(EmailPassword); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&email=iresh;john", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.users.length === 1); + }); + + it("test that search results correct output for 'email: iresh'", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(FastifyFramework.plugin); + await createUsers(EmailPassword); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&email=iresh", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.users.length === 0); + }); + + it("test that search results correct output for 'phone: +1'", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await createUsers(null, Passwordless); + await this.server.register(FastifyFramework.plugin); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&phone=%2B1", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.users.length === 3); + }); + + it("test that search results correct output for 'phone: 1('", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await createUsers(null, null); + await this.server.register(FastifyFramework.plugin); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&phone=1%28", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.users.length === 0); + }); + + it("test that search results correct output for 'provider: google'", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await createUsers(null, null, ThirdParty); + await this.server.register(FastifyFramework.plugin); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&provider=google", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.users.length === 3); + }); + + it("test that search results correct output for 'provider: google, phone: 1'", async function () { + await startST(); + SuperTokens.init({ + framework: "fastify", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await createUsers(null, Passwordless, ThirdParty); + await this.server.register(FastifyFramework.plugin); + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&provider=google&phone=1", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(resp.statusCode === 200); + const body = resp.json(); + assert(body.users.length === 0); + }); +}) diff --git a/test/framework/hapi.test.js b/test/framework/hapi.test.js deleted file mode 100644 index 2d08326ec..000000000 --- a/test/framework/hapi.test.js +++ /dev/null @@ -1,1899 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse } = require("../utils"); -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); -let SuperTokens = require("../../"); -let HapiFramework = require("../../framework/hapi"); -const Hapi = require("@hapi/hapi"); -let Session = require("../../recipe/session"); -let ThirdpartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -let { verifySession } = require("../../recipe/session/framework/hapi"); -let Dashboard = require("../../recipe/dashboard"); -let EmailPassword = require("../../recipe/emailpassword"); -const { createUsers } = require("../utils.js"); -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const Passwordless = require("../../recipe/passwordless"); -const ThirdParty = require("../../recipe/thirdparty"); -const { Apple, Google, Github } = require("../../recipe/thirdparty"); - -describe(`Hapi: ${printPath("[test/framework/hapi.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - this.server = Hapi.server({ - port: 3000, - host: "localhost", - }); - }); - - afterEach(async function () { - try { - await this.sever.stop(); - } catch (err) {} - }); - after(async function () { - await killAllST(); - await cleanST(); - }); - - // check if disabling api, the default refresh API does not work - you get a 404 - it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - this.server.route({ - method: "post", - path: "/create", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - assert(res2.statusCode === 404); - }); - - it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await this.server.inject({ - method: "post", - url: "/create", - }); - - let res2 = await this.server.inject({ - method: "post", - url: "/auth/signout", - }); - - assert(res2.statusCode === 404); - }); - - //- check for token theft detection - it("token theft detection", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { - response.sendJSONResponse({ - success: true, - }); - }, - }, - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - }), - ], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/verify", - handler: async (req, res) => { - await Session.getSession(req, res, true); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/auth/session/refresh", - handler: async (req, res) => { - await Session.refreshSession(req, res); - return res.response({ success: false }).code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - - let res3 = await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert.strictEqual(res3.result.success, true); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - }); - - //- check for token theft detection - it("token theft detection with auto refresh middleware", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/verify", - handler: async (req, res) => { - await Session.getSession(req, res, true); - return res.response("").code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - - let res3 = await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - assert.strictEqual(res3.statusCode, 401); - assert.deepStrictEqual(res3.result, { message: "token theft detected" }); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - }); - - //check basic usage of session - it("test basic usage of sessions", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/verify", - handler: async (req, res) => { - await Session.getSession(req, res, true); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/revoke", - handler: async (req, res) => { - let session = await Session.getSession(req, res, true); - await session.revokeSession(); - return res.response("").code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/session/revoke", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test signout API works", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/auth/signout", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - //check basic usage of session - it("test basic usage of sessions with auto refresh", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/verify", - handler: async (req, res) => { - await Session.getSession(req, res, true); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/revoke", - handler: async (req, res) => { - let session = await Session.getSession(req, res, true); - await session.revokeSession(); - return res.response("").code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/session/refresh", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }) - ); - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/session/revoke", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - // check session verify for with / without anti-csrf present - it("test session verify with anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/verify", - handler: async (req, res) => { - let sessionResponse = await Session.getSession(req, res, true); - return res.response({ userId: sessionResponse.userId }).code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/verifyAntiCsrfFalse", - handler: async (req, res) => { - let sessionResponse = await Session.getSession(req, res, false); - return res.response({ userId: sessionResponse.userId }).code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let res2 = await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert.strictEqual(res2.result.userId, "id1"); - - let res3 = await this.server.inject({ - method: "post", - url: "/session/verifyAntiCsrfFalse", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert.strictEqual(res3.result.userId, "id1"); - }); - - // check session verify for with / without anti-csrf present - it("test session verify without anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - await this.server.register(HapiFramework.plugin); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/verify", - handler: async (req, res) => { - try { - await Session.getSession(req, res, { antiCsrfCheck: true }); - return res.response({ success: false }).code(200); - } catch (err) { - return res - .response({ - success: err.type === Session.Error.TRY_REFRESH_TOKEN, - }) - .code(200); - } - }, - }); - - this.server.route({ - method: "post", - path: "/session/verifyAntiCsrfFalse", - handler: async (req, res) => { - let sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: false }); - return res.response({ userId: sessionResponse.userId }).code(200); - }, - }); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let response2 = await this.server.inject({ - method: "post", - url: "/session/verifyAntiCsrfFalse", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - }, - }); - assert.strictEqual(response2.result.userId, "id1"); - - let response = await this.server.inject({ - method: "post", - url: "/session/verify", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - }, - }); - assert.strictEqual(response.result.success, true); - }); - - //check revoking session(s)** - it("test revoking sessions", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - this.server.route({ - path: "/usercreate", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "someUniqueUserId", {}, {}); - return res.response("").code(200); - }, - }); - this.server.route({ - method: "post", - path: "/session/revoke", - handler: async (req, res) => { - let session = await Session.getSession(req, res, true); - await session.revokeSession(); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/revokeUserid", - handler: async (req, res) => { - let session = await Session.getSession(req, res, true); - await Session.revokeAllSessionsForUser(session.getUserId()); - return res.response("").code(200); - }, - }); - - this.server.route({ - method: "post", - path: "/session/getSessionsWithUserId1", - handler: async (req, res) => { - let sessionHandles = await Session.getAllSessionHandlesForUser("someUniqueUserId"); - return res.response(sessionHandles).code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - let sessionRevokedResponse = await this.server.inject({ - method: "post", - url: "/session/revoke", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - - await this.server.inject({ - method: "post", - url: "/usercreate", - }); - let userCreateResponse = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/usercreate", - }) - ); - - await this.server.inject({ - method: "post", - url: "/session/revokeUserid", - headers: { - Cookie: `sAccessToken=${userCreateResponse.accessToken}`, - "anti-csrf": userCreateResponse.antiCsrf, - }, - }); - let sessionHandleResponse = await this.server.inject({ - method: "post", - url: "/session/getSessionsWithUserId1", - }); - assert(sessionHandleResponse.result.length === 0); - }); - - //check manipulating session data - it("test manipulating session data", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - path: "/updateSessionData", - method: "post", - handler: async (req, res) => { - await req.session.updateSessionData({ key: "value" }); - return res.response("").code(200); - }, - options: { - pre: [{ method: verifySession() }], - }, - }); - - this.server.route({ - path: "/getSessionData", - method: "post", - handler: async (req, res) => { - let sessionData = await req.session.getSessionData(); - return res.response(sessionData).code(200); - }, - options: { - pre: [{ method: verifySession() }], - }, - }); - - this.server.route({ - path: "/updateSessionData2", - method: "post", - handler: async (req, res) => { - await req.session.updateSessionData(null); - return res.response("").code(200); - }, - options: { - pre: [{ method: verifySession() }], - }, - }); - - this.server.route({ - path: "/updateSessionDataInvalidSessionHandle", - method: "post", - handler: async (req, res) => { - return res - .response({ success: !(await Session.updateSessionData("InvalidHandle", { key: "value3" })) }) - .code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - //create a new session - let response = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - await this.server.inject({ - method: "post", - url: "/updateSessionData", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //call the getSessionData api to get session data - let response2 = await this.server.inject({ - method: "post", - url: "/getSessionData", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //check that the session data returned is valid - assert.strictEqual(response2.result.key, "value"); - - // change the value of the inserted session data - await this.server.inject({ - method: "post", - url: "/updateSessionData2", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //retrieve the changed session data - response2 = await this.server.inject({ - method: "post", - url: "/getSessionData", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - - //check the value of the retrieved - assert.deepStrictEqual(response2.result, {}); - - //invalid session handle when updating the session data - let invalidSessionResponse = await this.server.inject({ - method: "post", - url: "/updateSessionDataInvalidSessionHandle", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - assert.strictEqual(invalidSessionResponse.result.success, true); - }); - - //check manipulating jwt payload - it("test manipulating jwt payload", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.route({ - path: "/create", - method: "post", - handler: async (req, res) => { - await Session.createNewSession(req, res, "user1", {}, {}); - return res.response("").code(200); - }, - }); - - this.server.route({ - path: "/updateAccessTokenPayload", - method: "post", - handler: async (req, res) => { - let accessTokenBefore = req.session.accessToken; - await req.session.updateAccessTokenPayload({ key: "value" }); - let accessTokenAfter = req.session.accessToken; - let statusCode = - accessTokenBefore !== accessTokenAfter && typeof accessTokenAfter === "string" ? 200 : 500; - return res.response("").code(statusCode); - }, - options: { - pre: [{ method: verifySession() }], - }, - }); - - this.server.route({ - path: "/getAccessTokenPayload", - method: "post", - handler: async (req, res) => { - let jwtPayload = await req.session.getAccessTokenPayload(); - return res.response(jwtPayload).code(200); - }, - options: { - pre: [{ method: verifySession() }], - }, - }); - - this.server.route({ - path: "/updateAccessTokenPayload2", - method: "post", - handler: async (req, res) => { - await req.session.updateAccessTokenPayload(null); - return res.response("").code(200); - }, - options: { - pre: [ - { - method: verifySession({ - antiCsrfCheck: true, - }), - }, - ], - }, - }); - - this.server.route({ - path: "/updateAccessTokenPayloadInvalidSessionHandle", - method: "post", - handler: async (req, res) => { - return res - .response({ success: !(await Session.updateSessionData("InvalidHandle", { key: "value3" })) }) - .code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - //create a new session - let response = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/create", - }) - ); - - let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //call the updateAccessTokenPayload api to add jwt payload - let updatedResponse = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/updateAccessTokenPayload", - headers: { - Cookie: `sAccessToken=${response.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - //call the getAccessTokenPayload api to get jwt payload - let response2 = await this.server.inject({ - method: "post", - url: "/getAccessTokenPayload", - headers: { - Cookie: `sAccessToken=${updatedResponse.accessToken}`, - "anti-csrf": response.antiCsrf, - }, - }); - //check that the jwt payload returned is valid - assert.strictEqual(response2.result.key, "value"); - - // refresh session - response2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/auth/session/refresh", - headers: { - Cookie: `sRefreshToken=${response.refreshToken}`, - "anti-csrf": response.antiCsrf, - }, - }) - ); - - frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - // change the value of the inserted jwt payload - let updatedResponse2 = extractInfoFromResponse( - await this.server.inject({ - method: "post", - url: "/updateAccessTokenPayload2", - headers: { - Cookie: `sAccessToken=${response2.accessToken}`, - "anti-csrf": response2.antiCsrf, - }, - }) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //retrieve the changed jwt payload - let response3 = await this.server.inject({ - method: "post", - url: "/getAccessTokenPayload", - headers: { - Cookie: `sAccessToken=${updatedResponse2.accessToken}`, - "anti-csrf": response2.antiCsrf, - }, - }); - - //check the value of the retrieved - assert.deepStrictEqual(response3.result, {}); - //invalid session handle when updating the jwt payload - let invalidSessionResponse = await this.server.inject({ - method: "post", - url: "/updateAccessTokenPayloadInvalidSessionHandle", - headers: { - Cookie: `sAccessToken=${updatedResponse2.accessToken}`, - "anti-csrf": response2.antiCsrf, - }, - }); - assert.strictEqual(invalidSessionResponse.result.success, true); - }); - - it("sending custom response hapi", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailPasswordEmailExistsGET: async function (input) { - input.options.res.setStatusCode(203); - input.options.res.sendJSONResponse({ - custom: true, - }); - return oI.emailPasswordEmailExistsGET(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let response = await this.server.inject({ - method: "get", - url: "/auth/signup/email/exists?email=test@example.com", - }); - - assert(response.statusCode === 203); - assert(response.result.custom); - }); - - it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ], - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let res2 = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users/count", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(res2.statusCode === 200); - }); - - it("test verifySession/getSession without accessToken", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - this.server.route({ - path: "/getSessionV1", - method: "get", - handler: async (req, res) => { - return res.response({}).code(200); - }, - options: { - pre: [{ method: verifySession() }], - }, - }); - - this.server.route({ - path: "/getSessionV2", - method: "get", - handler: async (req, res) => { - await Session.getSession(req, res); - return res.response({}).code(200); - }, - }); - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - let response1 = await this.server.inject({ - method: "get", - url: "/getSessionV1", - }); - - assert.strictEqual(response1.statusCode, 401); - - let response2 = await this.server.inject({ - method: "get", - url: "/getSessionV2", - }); - - assert.strictEqual(response2.statusCode, 401); - }); - - it("test that tags request respond with correct tags", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(EmailPassword); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/search/tags", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.tags.length !== 0); - }); - - it("test that search results correct output for 'email: t'", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(EmailPassword); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&email=t", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.users.length === 5); - }); - - it("test that search results correct output for 'email: iresh'", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(EmailPassword); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&email=iresh", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.users.length === 0); - }); - it("test that search results correct output for multiple search items", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(EmailPassword); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&email=john;iresh", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.users.length === 1); - }); - - it("test that search results correct output for 'phone: +1'", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(null, Passwordless); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&phone=%2B1", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.users.length === 3); - }); - - it("test that search results correct output for 'phone: 1('", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(null, Passwordless); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&phone=1%28", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.users.length === 0); - }); - - it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(null, Passwordless, ThirdParty); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&provider=google&phone=1", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.users.length === 0); - }); - - it("test that search results correct output for 'provider: google'", async function () { - await startST(); - SuperTokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.server.register(HapiFramework.plugin); - - await this.server.initialize(); - - await createUsers(null, null, ThirdParty); - - let resp = await this.server.inject({ - method: "get", - url: "/auth/dashboard/api/users?limit=10&provider=google", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - assert(resp.statusCode === 200); - assert(resp.result.users.length === 3); - }); -}); diff --git a/test/framework/hapi.test.ts b/test/framework/hapi.test.ts new file mode 100644 index 000000000..7204f6396 --- /dev/null +++ b/test/framework/hapi.test.ts @@ -0,0 +1,1912 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import * as HapiFramework from 'supertokens-node/framework/hapi' +import Hapi from '@hapi/hapi' +import Session from 'supertokens-node/recipe/session' +import ThirdpartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword' +import { verifySession } from 'supertokens-node/recipe/session/framework/hapi' +import Dashboard from 'supertokens-node/recipe/dashboard' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, extractInfoFromResponse, killAllST, printPath, setupST, startST } from '../utils' + +import { Apple, Github, Google } from 'supertokens-node/recipe/thirdparty' +import { createUsers } from '../utils' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' + +import ThirdParty from 'supertokens-node/recipe/thirdparty' +import Passwordless from 'supertokens-node/recipe/passwordless' +import EmailPassword from 'supertokens-node/recipe/emailpassword' + + +describe(`Hapi: ${printPath('[test/framework/hapi.test.ts]')}`, () => { + let server: Hapi.Server + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + server = Hapi.server({ + port: 3000, + host: 'localhost', + }) + }) + + afterEach(async () => { + try { + await server.stop() + } + catch (err) {} + }) + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check if disabling api, the default refresh API does not work - you get a 404 + it('test that if disabling api, the default refresh API does not work', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + server.route({ + method: 'post', + path: '/create', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + assert(res2.statusCode === 404) + }) + + it('test that if disabling api, the default sign out API does not work', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + await server.inject({ + method: 'post', + url: '/create', + }) + + const res2 = await server.inject({ + method: 'post', + url: '/auth/signout', + }) + + assert(res2.statusCode === 404) + }) + + // - check for token theft detection + it('token theft detection', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + errorHandlers: { + onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + response.sendJSONResponse({ + success: true, + }) + }, + }, + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + }), + ], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/verify', + handler: async (req, res) => { + await Session.getSession(req, res, true) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/auth/session/refresh', + handler: async (req, res) => { + await Session.refreshSession(req, res) + return res.response({ success: false }).code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + + const res3 = await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert.strictEqual(res3.result.success, true) + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + }) + + // - check for token theft detection + it('token theft detection with auto refresh middleware', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/verify', + handler: async (req, res) => { + await Session.getSession(req, res, true) + return res.response('').code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + + const res3 = await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + assert.strictEqual(res3.statusCode, 401) + assert.deepStrictEqual(res3.result, { message: 'token theft detected' }) + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + }) + + // check basic usage of session + it('test basic usage of sessions', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/verify', + handler: async (req, res) => { + await Session.getSession(req, res, true) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/revoke', + handler: async (req, res) => { + const session = await Session.getSession(req, res, true) + await session.revokeSession() + return res.response('').code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/session/revoke', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test signout API works', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/auth/signout', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check basic usage of session + it('test basic usage of sessions with auto refresh', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/verify', + handler: async (req, res) => { + await Session.getSession(req, res, true) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/revoke', + handler: async (req, res) => { + const session = await Session.getSession(req, res, true) + await session.revokeSession() + return res.response('').code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }), + ) + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/session/revoke', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check session verify for with / without anti-csrf present + it('test session verify with anti-csrf present', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/verify', + handler: async (req, res) => { + const sessionResponse = await Session.getSession(req, res, true) + return res.response({ userId: sessionResponse.userId }).code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/verifyAntiCsrfFalse', + handler: async (req, res) => { + const sessionResponse = await Session.getSession(req, res, false) + return res.response({ userId: sessionResponse.userId }).code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const res2 = await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert.strictEqual(res2.result.userId, 'id1') + + const res3 = await server.inject({ + method: 'post', + url: '/session/verifyAntiCsrfFalse', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert.strictEqual(res3.result.userId, 'id1') + }) + + // check session verify for with / without anti-csrf present + it('test session verify without anti-csrf present', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + await server.register(HapiFramework.plugin) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/verify', + handler: async (req, res) => { + try { + await Session.getSession(req, res, { antiCsrfCheck: true }) + return res.response({ success: false }).code(200) + } + catch (err) { + return res + .response({ + success: err.type === Session.Error.TRY_REFRESH_TOKEN, + }) + .code(200) + } + }, + }) + + server.route({ + method: 'post', + path: '/session/verifyAntiCsrfFalse', + handler: async (req, res) => { + const sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: false }) + return res.response({ userId: sessionResponse.userId }).code(200) + }, + }) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + const response2 = await server.inject({ + method: 'post', + url: '/session/verifyAntiCsrfFalse', + headers: { + Cookie: `sAccessToken=${res.accessToken}`, + }, + }) + assert.strictEqual(response2.result.userId, 'id1') + + const response = await server.inject({ + method: 'post', + url: '/session/verify', + headers: { + Cookie: `sAccessToken=${res.accessToken}`, + }, + }) + assert.strictEqual(response.result.success, true) + }) + + // check revoking session(s)** + it('test revoking sessions', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + server.route({ + path: '/usercreate', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, 'someUniqueUserId', {}, {}) + return res.response('').code(200) + }, + }) + server.route({ + method: 'post', + path: '/session/revoke', + handler: async (req, res) => { + const session = await Session.getSession(req, res, true) + await session.revokeSession() + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/revokeUserid', + handler: async (req, res) => { + const session = await Session.getSession(req, res, true) + await Session.revokeAllSessionsForUser(session.getUserId()) + return res.response('').code(200) + }, + }) + + server.route({ + method: 'post', + path: '/session/getSessionsWithUserId1', + handler: async (req, res) => { + const sessionHandles = await Session.getAllSessionHandlesForUser('someUniqueUserId') + return res.response(sessionHandles).code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + const sessionRevokedResponse = await server.inject({ + method: 'post', + url: '/session/revoke', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + + await server.inject({ + method: 'post', + url: '/usercreate', + }) + const userCreateResponse = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/usercreate', + }), + ) + + await server.inject({ + method: 'post', + url: '/session/revokeUserid', + headers: { + 'Cookie': `sAccessToken=${userCreateResponse.accessToken}`, + 'anti-csrf': userCreateResponse.antiCsrf, + }, + }) + const sessionHandleResponse = await server.inject({ + method: 'post', + url: '/session/getSessionsWithUserId1', + }) + assert(sessionHandleResponse.result.length === 0) + }) + + // check manipulating session data + it('test manipulating session data', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + path: '/updateSessionData', + method: 'post', + handler: async (req, res) => { + await req.session.updateSessionData({ key: 'value' }) + return res.response('').code(200) + }, + options: { + pre: [{ method: verifySession() }], + }, + }) + + server.route({ + path: '/getSessionData', + method: 'post', + handler: async (req, res) => { + const sessionData = await req.session.getSessionData() + return res.response(sessionData).code(200) + }, + options: { + pre: [{ method: verifySession() }], + }, + }) + + server.route({ + path: '/updateSessionData2', + method: 'post', + handler: async (req, res) => { + await req.session.updateSessionData(null) + return res.response('').code(200) + }, + options: { + pre: [{ method: verifySession() }], + }, + }) + + server.route({ + path: '/updateSessionDataInvalidSessionHandle', + method: 'post', + handler: async (req, res) => { + return res + .response({ success: !(await Session.updateSessionData('InvalidHandle', { key: 'value3' })) }) + .code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + // create a new session + const response = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + await server.inject({ + method: 'post', + url: '/updateSessionData', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // call the getSessionData api to get session data + let response2 = await server.inject({ + method: 'post', + url: '/getSessionData', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // check that the session data returned is valid + assert.strictEqual(response2.result.key, 'value') + + // change the value of the inserted session data + await server.inject({ + method: 'post', + url: '/updateSessionData2', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // retrieve the changed session data + response2 = await server.inject({ + method: 'post', + url: '/getSessionData', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + + // check the value of the retrieved + assert.deepStrictEqual(response2.result, {}) + + // invalid session handle when updating the session data + const invalidSessionResponse = await server.inject({ + method: 'post', + url: '/updateSessionDataInvalidSessionHandle', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + assert.strictEqual(invalidSessionResponse.result.success, true) + }) + + // check manipulating jwt payload + it('test manipulating jwt payload', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + server.route({ + path: '/create', + method: 'post', + handler: async (req, res) => { + await Session.createNewSession(req, res, 'user1', {}, {}) + return res.response('').code(200) + }, + }) + + server.route({ + path: '/updateAccessTokenPayload', + method: 'post', + handler: async (req, res) => { + const accessTokenBefore = req.session.accessToken + await req.session.updateAccessTokenPayload({ key: 'value' }) + const accessTokenAfter = req.session.accessToken + const statusCode + = accessTokenBefore !== accessTokenAfter && typeof accessTokenAfter === 'string' ? 200 : 500 + return res.response('').code(statusCode) + }, + options: { + pre: [{ method: verifySession() }], + }, + }) + + server.route({ + path: '/getAccessTokenPayload', + method: 'post', + handler: async (req, res) => { + const jwtPayload = await req.session.getAccessTokenPayload() + return res.response(jwtPayload).code(200) + }, + options: { + pre: [{ method: verifySession() }], + }, + }) + + server.route({ + path: '/updateAccessTokenPayload2', + method: 'post', + handler: async (req, res) => { + await req.session.updateAccessTokenPayload(null) + return res.response('').code(200) + }, + options: { + pre: [ + { + method: verifySession({ + antiCsrfCheck: true, + }), + }, + ], + }, + }) + + server.route({ + path: '/updateAccessTokenPayloadInvalidSessionHandle', + method: 'post', + handler: async (req, res) => { + return res + .response({ success: !(await Session.updateSessionData('InvalidHandle', { key: 'value3' })) }) + .code(200) + }, + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + // create a new session + const response = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/create', + }), + ) + + let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // call the updateAccessTokenPayload api to add jwt payload + const updatedResponse = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/updateAccessTokenPayload', + headers: { + 'Cookie': `sAccessToken=${response.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + // call the getAccessTokenPayload api to get jwt payload + let response2 = await server.inject({ + method: 'post', + url: '/getAccessTokenPayload', + headers: { + 'Cookie': `sAccessToken=${updatedResponse.accessToken}`, + 'anti-csrf': response.antiCsrf, + }, + }) + // check that the jwt payload returned is valid + assert.strictEqual(response2.result.key, 'value') + + // refresh session + response2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/auth/session/refresh', + headers: { + 'Cookie': `sRefreshToken=${response.refreshToken}`, + 'anti-csrf': response.antiCsrf, + }, + }), + ) + + frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + // change the value of the inserted jwt payload + const updatedResponse2 = extractInfoFromResponse( + await server.inject({ + method: 'post', + url: '/updateAccessTokenPayload2', + headers: { + 'Cookie': `sAccessToken=${response2.accessToken}`, + 'anti-csrf': response2.antiCsrf, + }, + }), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // retrieve the changed jwt payload + const response3 = await server.inject({ + method: 'post', + url: '/getAccessTokenPayload', + headers: { + 'Cookie': `sAccessToken=${updatedResponse2.accessToken}`, + 'anti-csrf': response2.antiCsrf, + }, + }) + + // check the value of the retrieved + assert.deepStrictEqual(response3.result, {}) + // invalid session handle when updating the jwt payload + const invalidSessionResponse = await server.inject({ + method: 'post', + url: '/updateAccessTokenPayloadInvalidSessionHandle', + headers: { + 'Cookie': `sAccessToken=${updatedResponse2.accessToken}`, + 'anti-csrf': response2.antiCsrf, + }, + }) + assert.strictEqual(invalidSessionResponse.result.success, true) + }) + + it('sending custom response hapi', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + async emailPasswordEmailExistsGET(input) { + input.options.res.setStatusCode(203) + input.options.res.sendJSONResponse({ + custom: true, + }) + return oI.emailPasswordEmailExistsGET(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const response = await server.inject({ + method: 'get', + url: '/auth/signup/email/exists?email=test@example.com', + }) + + assert(response.statusCode === 203) + assert(response.result.custom) + }) + + it('test that authorization header is read correctly in dashboard recipe', async () => { + await startST() + SuperTokens.init({ + framework: 'hapi', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Dashboard.init({ + apiKey: 'testapikey', + override: { + functions: (original) => { + return { + ...original, + async shouldAllowAccess(input) { + const authHeader = input.req.getHeaderValue('authorization') + if (authHeader === 'Bearer testapikey') + return true + + return false + }, + } + }, + }, + }), + ], + }) + + await server.register(HapiFramework.plugin) + + await server.initialize() + + const res2 = await server.inject({ + method: 'get', + url: '/auth/dashboard/api/users/count', + headers: { + 'Authorization': 'Bearer testapikey', + 'Content-Type': 'application/json', + }, + }) + + assert(res2.statusCode === 200) + }) + + + + it("test verifySession/getSession without accessToken", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], + }); + + this.server.route({ + path: "/getSessionV1", + method: "get", + handler: async (req, res) => { + return res.response({}).code(200); + }, + options: { + pre: [{ method: verifySession() }], + }, + }); + + this.server.route({ + path: "/getSessionV2", + method: "get", + handler: async (req, res) => { + await Session.getSession(req, res); + return res.response({}).code(200); + }, + }); + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + let response1 = await this.server.inject({ + method: "get", + url: "/getSessionV1", + }); + + assert.strictEqual(response1.statusCode, 401); + + let response2 = await this.server.inject({ + method: "get", + url: "/getSessionV2", + }); + + assert.strictEqual(response2.statusCode, 401); + }); + + it("test that tags request respond with correct tags", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(EmailPassword); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/search/tags", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.tags.length !== 0); + }); + + it("test that search results correct output for 'email: t'", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(EmailPassword); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&email=t", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.users.length === 5); + }); + + it("test that search results correct output for 'email: iresh'", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(EmailPassword); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&email=iresh", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.users.length === 0); + }); + it("test that search results correct output for multiple search items", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(EmailPassword); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&email=john;iresh", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.users.length === 1); + }); + + it("test that search results correct output for 'phone: +1'", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(null, Passwordless); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&phone=%2B1", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.users.length === 3); + }); + + it("test that search results correct output for 'phone: 1('", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(null, Passwordless); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&phone=1%28", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.users.length === 0); + }); + + it("test that search results correct output for 'provider: google, phone: 1'", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(null, Passwordless, ThirdParty); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&provider=google&phone=1", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.users.length === 0); + }); + + it("test that search results correct output for 'provider: google'", async function () { + await startST(); + SuperTokens.init({ + framework: "hapi", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.server.register(HapiFramework.plugin); + + await this.server.initialize(); + + await createUsers(null, null, ThirdParty); + + let resp = await this.server.inject({ + method: "get", + url: "/auth/dashboard/api/users?limit=10&provider=google", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + assert(resp.statusCode === 200); + assert(resp.result.users.length === 3); + }); +}) diff --git a/test/framework/koa.test.js b/test/framework/koa.test.js deleted file mode 100644 index 7c29655c0..000000000 --- a/test/framework/koa.test.js +++ /dev/null @@ -1,2102 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse } = require("../utils"); -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); -let SuperTokens = require("../../"); -let KoaFramework = require("../../framework/koa"); -let Session = require("../../recipe/session"); -let EmailPassword = require("../../recipe/emailpassword"); -let Koa = require("koa"); -const Router = require("@koa/router"); -let { verifySession } = require("../../recipe/session/framework/koa"); -const request = require("supertest"); -let Dashboard = require("../../recipe/dashboard"); -const { createUsers } = require("../utils.js"); -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const Passwordless = require("../../recipe/passwordless"); -const ThirdParty = require("../../recipe/thirdparty"); -const { Google, Github, Apple } = require("../../recipe/thirdparty"); - -describe(`Koa: ${printPath("[test/framework/koa.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - this.server = undefined; - }); - - afterEach(function () { - if (this.server !== undefined) { - this.server.close(); - } - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // check if disabling api, the default refresh API does not work - you get a 404 - it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(this.server) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.statusCode === 404); - }); - - it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let res2 = await new Promise((resolve) => - request(this.server) - .post("/auth/signout") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.statusCode === 404); - }); - - //- check for token theft detection - it("koa token theft detection", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [ - Session.init({ - errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { - response.sendJSONResponse({ - success: true, - }); - }, - }, - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - }), - ], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - - router.post("/session/verify", async (ctx, next) => { - await Session.getSession(ctx, ctx, true); - ctx.body = ""; - }); - - router.post("/auth/session/refresh", async (ctx, next) => { - await Session.refreshSession(ctx, ctx); - ctx.body = { success: false }; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - resolve(); - }) - ); - - let res3 = await new Promise((resolve) => - request(this.server) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res3.body.success, true); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - }); - - //- check for token theft detection - it("koa token theft detection with auto refresh middleware", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - - router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - - router.post("/session/verify", verifySession(), async (ctx, _) => { - ctx.body = ""; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - resolve(); - }) - ); - - let res3 = await new Promise((resolve) => - request(this.server) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res3.status === 401); - assert.strictEqual(res3.text, '{"message":"token theft detected"}'); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - }); - - //check basic usage of session - it("test basic usage of express sessions", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - - router.post("/session/verify", async (ctx, next) => { - await Session.getSession(ctx, ctx, true); - ctx.body = ""; - }); - router.post("/auth/session/refresh", async (ctx, next) => { - await Session.refreshSession(ctx, ctx); - ctx.body = ""; - }); - router.post("/session/revoke", async (ctx, next) => { - let session = await Session.getSession(ctx, ctx, true); - await session.revokeSession(); - ctx.body = ""; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(this.server) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test signout API works", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let sessionRevokedResponse = await new Promise((resolve) => - request(this.server) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - //check basic usage of session - it("test basic usage of express sessions with auto refresh", async function () { - await startST(); - - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - apiBasePath: "/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - - router.post("/session/verify", verifySession(), async (ctx, next) => { - ctx.body = ""; - }); - - router.post("/session/revoke", verifySession(), async (ctx, next) => { - let session = ctx.session; - await session.revokeSession(); - ctx.body = ""; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(this.server) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - //check session verify for with / without anti-csrf present - it("test express session verify with anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "id1", {}, {}); - ctx.body = ""; - }); - - router.post("/session/verify", async (ctx, next) => { - let sessionResponse = await Session.getSession(ctx, ctx, { antiCsrfCheck: true }); - ctx.body = { userId: sessionResponse.userId }; - }); - - router.post("/session/verifyAntiCsrfFalse", async (ctx, next) => { - let sessionResponse = await Session.getSession(ctx, ctx, { antiCsrfCheck: false }); - ctx.body = { userId: sessionResponse.userId }; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res2.body.userId, "id1"); - - let res3 = await new Promise((resolve) => - request(this.server) - .post("/session/verifyAntiCsrfFalse") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res3.body.userId, "id1"); - }); - - // check session verify for with / without anti-csrf present - it("test session verify without anti-csrf present express", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - - router.post("/create", async (ctx, next) => { - await Session.createNewSession(ctx, ctx, "id1", {}, {}); - ctx.body = ""; - }); - - router.post("/session/verify", async (ctx, next) => { - try { - await Session.getSession(ctx, ctx, { antiCsrfCheck: true }); - ctx.body = { success: false }; - } catch (err) { - ctx.body = { - success: err.type === Session.Error.TRY_REFRESH_TOKEN, - }; - } - }); - - router.post("/session/verifyAntiCsrfFalse", async (ctx, next) => { - let sessionResponse = await Session.getSession(ctx, ctx, { antiCsrfCheck: false }); - ctx.body = { userId: sessionResponse.userId }; - }); - app.use(router.routes()); - this.server = app.listen(9999); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let response2 = await new Promise((resolve) => - request(this.server) - .post("/session/verifyAntiCsrfFalse") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response2.body.userId, "id1"); - - let response = await new Promise((resolve) => - request(this.server) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.body.success, true); - }); - - //check revoking session(s)** - it("test revoking express sessions", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - router.post("/usercreate", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "someUniqueUserId", {}, {}); - ctx.body = ""; - }); - router.post("/session/revoke", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - await session.revokeSession(); - ctx.body = ""; - }); - - router.post("/session/revokeUserid", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - await Session.revokeAllSessionsForUser(session.getUserId()); - ctx.body = ""; - }); - - //create an api call get sesssions from a userid "id1" that returns all the sessions for that userid - router.post("/session/getSessionsWithUserId1", async (ctx, _) => { - let sessionHandles = await Session.getAllSessionHandlesForUser("someUniqueUserId"); - ctx.body = sessionHandles; - }); - app.use(router.routes()); - this.server = app.listen(9999); - - let response = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let sessionRevokedResponse = await new Promise((resolve) => - request(this.server) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - - await new Promise((resolve) => - request(this.server) - .post("/usercreate") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let userCreateResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/usercreate") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(this.server) - .post("/session/revokeUserid") - .set("Cookie", ["sAccessToken=" + userCreateResponse.accessToken]) - .set("anti-csrf", userCreateResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionHandleResponse = await new Promise((resolve) => - request(this.server) - .post("/session/getSessionsWithUserId1") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(sessionHandleResponse.body.length === 0); - }); - - //check manipulating session data - it("test manipulating session data with express", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "", {}, {}); - ctx.body = ""; - }); - router.post("/updateSessionData", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - await session.updateSessionData({ key: "value" }); - ctx.body = ""; - }); - router.post("/getSessionData", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - let sessionData = await session.getSessionData(); - ctx.body = sessionData; - }); - - router.post("/updateSessionData2", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - await session.updateSessionData(null); - ctx.body = ""; - }); - - router.post("/updateSessionDataInvalidSessionHandle", async (ctx, _) => { - ctx.body = { success: !(await Session.updateSessionData("InvalidHandle", { key: "value3" })) }; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - //create a new session - let response = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - //call the updateSessionData api to add session data - await new Promise((resolve) => - request(this.server) - .post("/updateSessionData") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //call the getSessionData api to get session data - let response2 = await new Promise((resolve) => - request(this.server) - .post("/getSessionData") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //check that the session data returned is valid - assert.strictEqual(response2.body.key, "value"); - - // change the value of the inserted session data - await new Promise((resolve) => - request(this.server) - .post("/updateSessionData2") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - //retrieve the changed session data - response2 = await new Promise((resolve) => - request(this.server) - .post("/getSessionData") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //check the value of the retrieved - assert.deepStrictEqual(response2.body, {}); - - //invalid session handle when updating the session data - let invalidSessionResponse = await new Promise((resolve) => - request(this.server) - .post("/updateSessionDataInvalidSessionHandle") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(invalidSessionResponse.body.success, true); - }); - - //check manipulating jwt payload - it("test manipulating jwt payload with express", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "http://api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "http://supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - let app = new Koa(); - const router = new Router(); - app.use(KoaFramework.middleware()); - router.post("/create", async (ctx, _) => { - await Session.createNewSession(ctx, ctx, "user1", {}, {}); - ctx.body = ""; - }); - router.post("/updateAccessTokenPayload", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - let accessTokenBefore = session.accessToken; - await session.updateAccessTokenPayload({ key: "value" }); - let accessTokenAfter = session.accessToken; - let statusCode = accessTokenBefore !== accessTokenAfter && typeof accessTokenAfter === "string" ? 200 : 500; - ctx.status = statusCode; - ctx.body = ""; - }); - router.post("/getAccessTokenPayload", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - let jwtPayload = session.getAccessTokenPayload(); - ctx.body = jwtPayload; - }); - - router.post("/updateAccessTokenPayload2", async (ctx, _) => { - let session = await Session.getSession(ctx, ctx, true); - await session.updateAccessTokenPayload(null); - ctx.body = ""; - }); - - router.post("/updateAccessTokenPayloadInvalidSessionHandle", async (ctx, _) => { - ctx.body = { - success: !(await Session.updateAccessTokenPayload("InvalidHandle", { key: "value3" })), - }; - }); - - app.use(router.routes()); - this.server = app.listen(9999); - - //create a new session - let response = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //call the updateAccessTokenPayload api to add jwt payload - let updatedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/updateAccessTokenPayload") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - //call the getAccessTokenPayload api to get jwt payload - let response2 = await new Promise((resolve) => - request(this.server) - .post("/getAccessTokenPayload") - .set("Cookie", ["sAccessToken=" + updatedResponse.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - //check that the jwt payload returned is valid - assert.strictEqual(response2.body.key, "value"); - - // refresh session - response2 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + response.refreshToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - // change the value of the inserted jwt payload - let updatedResponse2 = extractInfoFromResponse( - await new Promise((resolve) => - request(this.server) - .post("/updateAccessTokenPayload2") - .set("Cookie", ["sAccessToken=" + response2.accessToken]) - .set("anti-csrf", response2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //retrieve the changed jwt payload - response2 = await new Promise((resolve) => - request(this.server) - .post("/getAccessTokenPayload") - .set("Cookie", ["sAccessToken=" + updatedResponse2.accessToken]) - .set("anti-csrf", response2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //check the value of the retrieved - assert.deepStrictEqual(response2.body, {}); - //invalid session handle when updating the jwt payload - let invalidSessionResponse = await new Promise((resolve) => - request(this.server) - .post("/updateAccessTokenPayloadInvalidSessionHandle") - .set("Cookie", ["sAccessToken=" + updatedResponse2.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(invalidSessionResponse.body.success, true); - }); - - it("sending custom response koa", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailExistsGET: async function (input) { - input.options.res.setStatusCode(203); - input.options.res.sendJSONResponse({ - custom: true, - }); - return oI.emailExistsGET(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - let response = await new Promise((resolve) => - request(this.server) - .get("/auth/signup/email/exists?email=test@example.com") - .expect(203) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.body.custom); - }); - - it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - if (authHeader === "Bearer testapikey") { - return true; - } - - return false; - }, - }; - }, - }, - }), - ], - }); - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users/count") - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res.statusCode === 200); - }); - - it("test that tags request respond with correct tags", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/search/tags") - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - const tags = res.body.tags; - assert(tags.length !== 0); - }); - - it("test that search results correct output for 'email: t'", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - await createUsers(EmailPassword); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users") - .query({ - email: "t", - limit: 10, - }) - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - assert(res.body.users.length === 5); - }); - - it("test that search results correct output for multiple search items", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - await createUsers(EmailPassword); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users") - .query({ - email: "iresh;john", - limit: 10, - }) - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - assert(res.body.users.length === 1); - }); - - it("test that search results correct output for 'email: iresh'", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - await createUsers(EmailPassword); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users") - .query({ - email: "iresh", - limit: 10, - }) - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - assert(res.body.users.length === 0); - }); - - it("test that search results correct output for 'phone: +1'", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - await createUsers(null, Passwordless); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users") - .query({ - phone: "+1", - limit: 10, - }) - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - assert(res.body.users.length === 3); - }); - - it("test that search results correct output for 'phone: 1('", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - await createUsers(null, Passwordless); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users") - .query({ - phone: "1(", - limit: 10, - }) - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - assert(res.body.users.length === 0); - }); - - it("test that search results correct output for 'provider: google'", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - await createUsers(null, null, ThirdParty); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users") - .query({ - provider: "google", - limit: 10, - }) - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - assert(res.body.users.length === 3); - }); - - it("test that search results correct output for 'provider: google, phone: 1'", async function () { - await startST(); - SuperTokens.init({ - framework: "koa", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - const app = new Koa(); - app.use(KoaFramework.middleware()); - this.server = app.listen(9999); - - await createUsers(null, Passwordless, ThirdParty); - - let res = await new Promise((resolve) => - request(this.server) - .get("/auth/dashboard/api/users") - .query({ - provider: "google", - phone: "1", - limit: 10, - }) - .set("Content-Type", "application/json") - .set("Authorization", "Bearer testapikey") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.statusCode === 200); - assert(res.body.users.length === 0); - }); -}); diff --git a/test/framework/koa.test.ts b/test/framework/koa.test.ts new file mode 100644 index 000000000..0a8fbc900 --- /dev/null +++ b/test/framework/koa.test.ts @@ -0,0 +1,2103 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { IncomingMessage, Server, ServerResponse } from 'http' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import * as KoaFramework from 'supertokens-node/framework/koa' +import Session from 'supertokens-node/recipe/session' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import Koa from 'koa' +import Router from '@koa/router' +import { verifySession } from 'supertokens-node/recipe/session/framework/koa' +import request from 'supertest' +import Dashboard from 'supertokens-node/recipe/dashboard' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, extractInfoFromResponse, killAllST, printPath, setupST, startST } from '../utils' + + +import { Apple, Github, Google } from 'supertokens-node/recipe/thirdparty' +import { createUsers } from '../utils' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' + +import ThirdParty from 'supertokens-node/recipe/thirdparty' +import Passwordless from 'supertokens-node/recipe/passwordless' + +describe(`Koa: ${printPath('[test/framework/koa.test.ts]')}`, () => { + let server: Server + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + server = undefined as any + }) + + afterEach(() => { + if (server !== undefined) + server.close() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check if disabling api, the default refresh API does not work - you get a 404 + it('test that if disabling api, the default refresh API does not work', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(server) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.statusCode === 404) + }) + + it('test that if disabling api, the default sign out API does not work', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + + app.use(router.routes()) + server = app.listen(9999) + + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const res2 = await new Promise(resolve => + request(server) + .post('/auth/signout') + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + + assert(res2.statusCode === 404) + }) + + // - check for token theft detection + it('koa token theft detection', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [ + Session.init({ + errorHandlers: { + onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + response.sendJSONResponse({ + success: true, + }) + }, + }, + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + }), + ], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + + router.post('/session/verify', async (ctx, next) => { + await Session.getSession(ctx, ctx, true) + ctx.body = '' + }) + + router.post('/auth/session/refresh', async (ctx, next) => { + await Session.refreshSession(ctx, ctx) + ctx.body = { success: false } + }) + + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + resolve() + }), + ) + + const res3 = await new Promise(resolve => + request(server) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert.strictEqual(res3.body.success, true) + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + }) + + // - check for token theft detection + it('koa token theft detection with auto refresh middleware', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + + router.post('/create', async (ctx, _) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + + router.post('/session/verify', verifySession(), async (ctx, _) => { + ctx.body = '' + }) + + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + resolve() + }), + ) + + const res3 = await new Promise(resolve => + request(server) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert(res3.status === 401) + assert.strictEqual(res3.text, '{"message":"token theft detected"}') + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + }) + + // check basic usage of session + it('test basic usage of express sessions', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + + router.post('/session/verify', async (ctx, next) => { + await Session.getSession(ctx, ctx, true) + ctx.body = '' + }) + router.post('/auth/session/refresh', async (ctx, next) => { + await Session.refreshSession(ctx, ctx) + ctx.body = '' + }) + router.post('/session/revoke', async (ctx, next) => { + const session = await Session.getSession(ctx, ctx, true) + await session.revokeSession() + ctx.body = '' + }) + + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(server) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test signout API works', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const sessionRevokedResponse = await new Promise(resolve => + request(server) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check basic usage of session + it('test basic usage of express sessions with auto refresh', async () => { + await startST() + + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + apiBasePath: '/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + + router.post('/session/verify', verifySession(), async (ctx, next) => { + ctx.body = '' + }) + + router.post('/session/revoke', verifySession(), async (ctx, next) => { + const session = ctx.session + await session.revokeSession() + ctx.body = '' + }) + + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(server) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check session verify for with / without anti-csrf present + it('test express session verify with anti-csrf present', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, 'id1', {}, {}) + ctx.body = '' + }) + + router.post('/session/verify', async (ctx, next) => { + const sessionResponse = await Session.getSession(ctx, ctx, { antiCsrfCheck: true }) + ctx.body = { userId: sessionResponse.userId } + }) + + router.post('/session/verifyAntiCsrfFalse', async (ctx, next) => { + const sessionResponse = await Session.getSession(ctx, ctx, { antiCsrfCheck: false }) + ctx.body = { userId: sessionResponse.userId } + }) + + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(res2.body.userId, 'id1') + + const res3 = await new Promise(resolve => + request(server) + .post('/session/verifyAntiCsrfFalse') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(res3.body.userId, 'id1') + }) + + // check session verify for with / without anti-csrf present + it('test session verify without anti-csrf present express', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + + router.post('/create', async (ctx, next) => { + await Session.createNewSession(ctx, ctx, 'id1', {}, {}) + ctx.body = '' + }) + + router.post('/session/verify', async (ctx, next) => { + try { + await Session.getSession(ctx, ctx, { antiCsrfCheck: true }) + ctx.body = { success: false } + } + catch (err) { + ctx.body = { + success: err.type === Session.Error.TRY_REFRESH_TOKEN, + } + } + }) + + router.post('/session/verifyAntiCsrfFalse', async (ctx, next) => { + const sessionResponse = await Session.getSession(ctx, ctx, { antiCsrfCheck: false }) + ctx.body = { userId: sessionResponse.userId } + }) + app.use(router.routes()) + server = app.listen(9999) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const response2 = await new Promise(resolve => + request(server) + .post('/session/verifyAntiCsrfFalse') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response2.body.userId, 'id1') + + const response = await new Promise(resolve => + request(server) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.body.success, true) + }) + + // check revoking session(s)** + it('test revoking express sessions', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + router.post('/create', async (ctx, _) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + router.post('/usercreate', async (ctx, _) => { + await Session.createNewSession(ctx, ctx, 'someUniqueUserId', {}, {}) + ctx.body = '' + }) + router.post('/session/revoke', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + await session.revokeSession() + ctx.body = '' + }) + + router.post('/session/revokeUserid', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + await Session.revokeAllSessionsForUser(session.getUserId()) + ctx.body = '' + }) + + // create an api call get sesssions from a userid "id1" that returns all the sessions for that userid + router.post('/session/getSessionsWithUserId1', async (ctx, _) => { + const sessionHandles = await Session.getAllSessionHandlesForUser('someUniqueUserId') + ctx.body = sessionHandles + }) + app.use(router.routes()) + server = app.listen(9999) + + const response = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const sessionRevokedResponse = await new Promise(resolve => + request(server) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + + await new Promise(resolve => + request(server) + .post('/usercreate') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const userCreateResponse = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/usercreate') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(server) + .post('/session/revokeUserid') + .set('Cookie', [`sAccessToken=${userCreateResponse.accessToken}`]) + .set('anti-csrf', userCreateResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionHandleResponse = await new Promise(resolve => + request(server) + .post('/session/getSessionsWithUserId1') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert(sessionHandleResponse.body.length === 0) + }) + + // check manipulating session data + it('test manipulating session data with express', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + router.post('/create', async (ctx, _) => { + await Session.createNewSession(ctx, ctx, '', {}, {}) + ctx.body = '' + }) + router.post('/updateSessionData', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + await session.updateSessionData({ key: 'value' }) + ctx.body = '' + }) + router.post('/getSessionData', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + const sessionData = await session.getSessionData() + ctx.body = sessionData + }) + + router.post('/updateSessionData2', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + await session.updateSessionData(null) + ctx.body = '' + }) + + router.post('/updateSessionDataInvalidSessionHandle', async (ctx, _) => { + ctx.body = { success: !(await Session.updateSessionData('InvalidHandle', { key: 'value3' })) } + }) + + app.use(router.routes()) + server = app.listen(9999) + + // create a new session + const response = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + // call the updateSessionData api to add session data + await new Promise(resolve => + request(server) + .post('/updateSessionData') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // call the getSessionData api to get session data + let response2 = await new Promise(resolve => + request(server) + .post('/getSessionData') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // check that the session data returned is valid + assert.strictEqual(response2.body.key, 'value') + + // change the value of the inserted session data + await new Promise(resolve => + request(server) + .post('/updateSessionData2') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + // retrieve the changed session data + response2 = await new Promise(resolve => + request(server) + .post('/getSessionData') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // check the value of the retrieved + assert.deepStrictEqual(response2.body, {}) + + // invalid session handle when updating the session data + const invalidSessionResponse = await new Promise(resolve => + request(server) + .post('/updateSessionDataInvalidSessionHandle') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(invalidSessionResponse.body.success, true) + }) + + // check manipulating jwt payload + it('test manipulating jwt payload with express', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'http://api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'http://supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = new Koa() + const router = new Router() + app.use(KoaFramework.middleware()) + router.post('/create', async (ctx, _) => { + await Session.createNewSession(ctx, ctx, 'user1', {}, {}) + ctx.body = '' + }) + router.post('/updateAccessTokenPayload', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + const accessTokenBefore = session.accessToken + await session.updateAccessTokenPayload({ key: 'value' }) + const accessTokenAfter = session.accessToken + const statusCode = accessTokenBefore !== accessTokenAfter && typeof accessTokenAfter === 'string' ? 200 : 500 + ctx.status = statusCode + ctx.body = '' + }) + router.post('/getAccessTokenPayload', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + const jwtPayload = session.getAccessTokenPayload() + ctx.body = jwtPayload + }) + + router.post('/updateAccessTokenPayload2', async (ctx, _) => { + const session = await Session.getSession(ctx, ctx, true) + await session.updateAccessTokenPayload(null) + ctx.body = '' + }) + + router.post('/updateAccessTokenPayloadInvalidSessionHandle', async (ctx, _) => { + ctx.body = { + success: !(await Session.updateAccessTokenPayload('InvalidHandle', { key: 'value3' })), + } + }) + + app.use(router.routes()) + server = app.listen(9999) + + // create a new session + const response = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // call the updateAccessTokenPayload api to add jwt payload + const updatedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/updateAccessTokenPayload') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + // call the getAccessTokenPayload api to get jwt payload + let response2 = await new Promise(resolve => + request(server) + .post('/getAccessTokenPayload') + .set('Cookie', [`sAccessToken=${updatedResponse.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + // check that the jwt payload returned is valid + assert.strictEqual(response2.body.key, 'value') + + // refresh session + response2 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${response.refreshToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + // change the value of the inserted jwt payload + const updatedResponse2 = extractInfoFromResponse( + await new Promise(resolve => + request(server) + .post('/updateAccessTokenPayload2') + .set('Cookie', [`sAccessToken=${response2.accessToken}`]) + .set('anti-csrf', response2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // retrieve the changed jwt payload + response2 = await new Promise(resolve => + request(server) + .post('/getAccessTokenPayload') + .set('Cookie', [`sAccessToken=${updatedResponse2.accessToken}`]) + .set('anti-csrf', response2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // check the value of the retrieved + assert.deepStrictEqual(response2.body, {}) + // invalid session handle when updating the jwt payload + const invalidSessionResponse = await new Promise(resolve => + request(server) + .post('/updateAccessTokenPayloadInvalidSessionHandle') + .set('Cookie', [`sAccessToken=${updatedResponse2.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(invalidSessionResponse.body.success, true) + }) + + it('sending custom response koa', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + async emailExistsGET(input) { + input.options.res.setStatusCode(203) + input.options.res.sendJSONResponse({ + custom: true, + }) + return oI.emailExistsGET(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = new Koa() + app.use(KoaFramework.middleware()) + server = app.listen(9999) + + const response = await new Promise(resolve => + request(server) + .get('/auth/signup/email/exists?email=test@example.com') + .expect(203) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.body.custom) + }) + + it('test that authorization header is read correctly in dashboard recipe', async () => { + await startST() + SuperTokens.init({ + framework: 'koa', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Dashboard.init({ + apiKey: 'testapikey', + override: { + functions: (original) => { + return { + ...original, + async shouldAllowAccess(input) { + const authHeader = input.req.getHeaderValue('authorization') + if (authHeader === 'Bearer testapikey') + return true + + return false + }, + } + }, + }, + }), + ], + }) + + const app = new Koa() + app.use(KoaFramework.middleware()) + server = app.listen(9999) + + const res = await new Promise(resolve => + request(server) + .get('/auth/dashboard/api/users/count') + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer testapikey') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res.statusCode === 200) + }) + + it("test that tags request respond with correct tags", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/search/tags") + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + const tags = res.body.tags; + assert(tags.length !== 0); + }); + + it("test that search results correct output for 'email: t'", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + await createUsers(EmailPassword); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/users") + .query({ + email: "t", + limit: 10, + }) + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + assert(res.body.users.length === 5); + }); + + it("test that search results correct output for multiple search items", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + await createUsers(EmailPassword); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/users") + .query({ + email: "iresh;john", + limit: 10, + }) + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + assert(res.body.users.length === 1); + }); + + it("test that search results correct output for 'email: iresh'", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + await createUsers(EmailPassword); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/users") + .query({ + email: "iresh", + limit: 10, + }) + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + assert(res.body.users.length === 0); + }); + + it("test that search results correct output for 'phone: +1'", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + await createUsers(null, Passwordless); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/users") + .query({ + phone: "+1", + limit: 10, + }) + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + assert(res.body.users.length === 3); + }); + + it("test that search results correct output for 'phone: 1('", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + await createUsers(null, Passwordless); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/users") + .query({ + phone: "1(", + limit: 10, + }) + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + assert(res.body.users.length === 0); + }); + + it("test that search results correct output for 'provider: google'", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + await createUsers(null, null, ThirdParty); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/users") + .query({ + provider: "google", + limit: 10, + }) + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + assert(res.body.users.length === 3); + }); + + it("test that search results correct output for 'provider: google, phone: 1'", async function () { + await startST(); + SuperTokens.init({ + framework: "koa", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + const app = new Koa(); + app.use(KoaFramework.middleware()); + this.server = app.listen(9999); + + await createUsers(null, Passwordless, ThirdParty); + + let res = await new Promise((resolve) => + request(this.server) + .get("/auth/dashboard/api/users") + .query({ + provider: "google", + phone: "1", + limit: 10, + }) + .set("Content-Type", "application/json") + .set("Authorization", "Bearer testapikey") + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + assert(res.statusCode === 200); + assert(res.body.users.length === 0); + }); + +}) diff --git a/test/framework/loopback-server/index.js b/test/framework/loopback-server/index.js deleted file mode 100644 index 15256a6c9..000000000 --- a/test/framework/loopback-server/index.js +++ /dev/null @@ -1,123 +0,0 @@ -"use strict"; -var __decorate = - (this && this.__decorate) || - function (decorators, target, key, desc) { - var c = arguments.length, - r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, - d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") - r = Reflect.decorate(decorators, target, key, desc); - else - for (var i = decorators.length - 1; i >= 0; i--) - if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; - }; -var __param = - (this && this.__param) || - function (paramIndex, decorator) { - return function (target, key) { - decorator(target, key, paramIndex); - }; - }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const core_1 = require("@loopback/core"); -const rest_1 = require("@loopback/rest"); -const loopback_1 = require("../../../framework/loopback"); -const loopback_2 = require("../../../recipe/session/framework/loopback"); -const session_1 = __importDefault(require("../../../recipe/session")); -let Create = class Create { - constructor(ctx) { - this.ctx = ctx; - } - async handler() { - await session_1.default.createNewSession(this.ctx, this.ctx, "userId", {}, {}); - return {}; - } -}; -__decorate([rest_1.post("/create"), rest_1.response(200)], Create.prototype, "handler", null); -Create = __decorate([__param(0, core_1.inject(rest_1.RestBindings.Http.CONTEXT))], Create); -let CreateThrowing = class CreateThrowing { - constructor(ctx) { - this.ctx = ctx; - } - async handler() { - await session_1.default.createNewSession(this.ctx, this.ctx, "userId", {}, {}); - throw new session_1.default.Error({ - message: "unauthorised", - type: session_1.default.Error.UNAUTHORISED, - }); - } -}; -__decorate([rest_1.post("/create-throw"), rest_1.response(200)], CreateThrowing.prototype, "handler", null); -CreateThrowing = __decorate([__param(0, core_1.inject(rest_1.RestBindings.Http.CONTEXT))], CreateThrowing); -let Verify = class Verify { - constructor(ctx) { - this.ctx = ctx; - } - handler() { - return { - user: this.ctx.session.getUserId(), - }; - } -}; -__decorate( - [rest_1.post("/session/verify"), core_1.intercept(loopback_2.verifySession()), rest_1.response(200)], - Verify.prototype, - "handler", - null -); -Verify = __decorate([__param(0, core_1.inject(rest_1.RestBindings.Http.CONTEXT))], Verify); -let VerifyOptionalCSRF = class VerifyOptionalCSRF { - constructor(ctx) { - this.ctx = ctx; - } - handler() { - return { - user: this.ctx.session.getUserId(), - }; - } -}; -__decorate( - [ - rest_1.post("/session/verify/optionalCSRF"), - core_1.intercept(loopback_2.verifySession({ antiCsrfCheck: false })), - rest_1.response(200), - ], - VerifyOptionalCSRF.prototype, - "handler", - null -); -VerifyOptionalCSRF = __decorate([__param(0, core_1.inject(rest_1.RestBindings.Http.CONTEXT))], VerifyOptionalCSRF); -let Revoke = class Revoke { - constructor(ctx) { - this.ctx = ctx; - } - async handler() { - await this.ctx.session.revokeSession(); - return {}; - } -}; -__decorate( - [rest_1.post("/session/revoke"), core_1.intercept(loopback_2.verifySession()), rest_1.response(200)], - Revoke.prototype, - "handler", - null -); -Revoke = __decorate([__param(0, core_1.inject(rest_1.RestBindings.Http.CONTEXT))], Revoke); -let app = new rest_1.RestApplication({ - rest: { - port: 9876, - }, -}); -app.middleware(loopback_1.middleware); -app.controller(Create); -app.controller(CreateThrowing); -app.controller(Verify); -app.controller(Revoke); -app.controller(VerifyOptionalCSRF); -module.exports = app; diff --git a/test/framework/loopback-server/index.ts b/test/framework/loopback-server/index.ts index 6bbc40a87..eb8bf01b9 100644 --- a/test/framework/loopback-server/index.ts +++ b/test/framework/loopback-server/index.ts @@ -1,76 +1,77 @@ -import { intercept, inject } from "@loopback/core"; -import { post, response, RestApplication, RestBindings, MiddlewareContext } from "@loopback/rest"; -import { middleware } from "../../../framework/loopback"; -import { verifySession } from "../../../recipe/session/framework/loopback"; -import Session from "../../../recipe/session"; +import { inject, intercept } from '@loopback/core' +import { MiddlewareContext, RestApplication, RestBindings, post, response } from '@loopback/rest' +import { middleware } from 'supertokens-node/framework/loopback' +import { verifySession } from 'supertokens-node/recipe/session/framework/loopback' +import Session, { SessionContainer } from 'supertokens-node/recipe/session' class Create { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} - @post("/create") - @response(200) - async handler() { - await Session.createNewSession(this.ctx, this.ctx, "userId", {}, {}); - return {}; - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post('/create') + @response(200) + async handler() { + await Session.createNewSession(this.ctx, this.ctx, 'userId', {}, {}) + return {} + } } class CreateThrowing { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} - @post("/create-throw") - @response(200) - async handler() { - await Session.createNewSession(this.ctx, this.ctx, "userId", {}, {}); - throw new Session.Error({ - message: "unauthorised", - type: Session.Error.UNAUTHORISED, - }); - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post('/create-throw') + @response(200) + async handler() { + await Session.createNewSession(this.ctx, this.ctx, 'userId', {}, {}) + throw new Session.Error({ + message: 'unauthorised', + type: Session.Error.UNAUTHORISED, + }) + } } class Verify { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} - @post("/session/verify") - @intercept(verifySession()) - @response(200) - handler() { - return { - user: ((this.ctx as any).session as Session.SessionContainer).getUserId(), - }; + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post('/session/verify') + @intercept(verifySession()) + @response(200) + handler() { + return { + user: ((this.ctx as any).session as SessionContainer).getUserId(), } + } } class VerifyOptionalCSRF { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} - @post("/session/verify/optionalCSRF") - @intercept(verifySession({ antiCsrfCheck: false })) - @response(200) - handler() { - return { - user: ((this.ctx as any).session as Session.SessionContainer).getUserId(), - }; + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post('/session/verify/optionalCSRF') + @intercept(verifySession({ antiCsrfCheck: false })) + @response(200) + handler() { + return { + user: ((this.ctx as any).session as SessionContainer).getUserId(), } + } } class Revoke { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} - @post("/session/revoke") - @intercept(verifySession()) - @response(200) - async handler() { - await ((this.ctx as any).session as Session.SessionContainer).revokeSession(); - return {}; - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post('/session/revoke') + @intercept(verifySession()) + @response(200) + async handler() { + await ((this.ctx as any).session as SessionContainer).revokeSession() + return {} + } } -let app = new RestApplication({ - rest: { - port: 9876, - }, -}); +const app = new RestApplication({ + rest: { + port: 9876, + }, +}) -app.middleware(middleware); -app.controller(Create); -app.controller(CreateThrowing); -app.controller(Verify); -app.controller(Revoke); -app.controller(VerifyOptionalCSRF); -module.exports = app; +app.middleware(middleware) +app.controller(Create) +app.controller(CreateThrowing) +app.controller(Verify) +app.controller(Revoke) +app.controller(VerifyOptionalCSRF) +export { app } +module.exports = app diff --git a/test/framework/loopback-server/tsconfig.json b/test/framework/loopback-server/tsconfig.json index af9c6e885..1a91ea5c1 100644 --- a/test/framework/loopback-server/tsconfig.json +++ b/test/framework/loopback-server/tsconfig.json @@ -5,12 +5,22 @@ "strict": true, // FIXME(bajtos) LB4 is not compatible with this setting yet "strictPropertyInitialization": false, - "lib": ["es2020"], + "lib": [ + "es2020" + ], "module": "commonjs", "esModuleInterop": true, "moduleResolution": "node", "target": "es2018", - "outDir": "." + "outDir": ".", + "paths": { + "supertokens-node/*": [ + "../../../src/*" + ], + "supertokens-node": [ + "../../../src/index.ts" + ] + } }, "exclude": [] -} +} \ No newline at end of file diff --git a/test/framework/loopback.test.js b/test/framework/loopback.test.js deleted file mode 100644 index 33e819ef9..000000000 --- a/test/framework/loopback.test.js +++ /dev/null @@ -1,809 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse } = require("../utils"); -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); -let SuperTokens = require("../.."); -let { middleware } = require("../../framework/awsLambda"); -let Session = require("../../recipe/session"); -let EmailPassword = require("../../recipe/emailpassword"); -let { verifySession } = require("../../recipe/session/framework/awsLambda"); -const request = require("supertest"); -const axios = require("axios").default; -let Dashboard = require("../../recipe/dashboard"); -const { createUsers } = require("../utils.js"); -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const Passwordless = require("../../recipe/passwordless"); -const ThirdParty = require("../../recipe/thirdparty"); -const { Apple, Google, Github } = require("../../recipe/thirdparty"); - -describe(`Loopback: ${printPath("[test/framework/loopback.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - this.app = require("./loopback-server/index.js"); - }); - - afterEach(async function () { - if (this.app !== undefined) { - await this.app.stop(); - } - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - //check basic usage of session - it("test basic usage of sessions", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - await this.app.start(); - - let result = await axios({ - url: "/create", - baseURL: "http://localhost:9876", - method: "post", - }); - let res = extractInfoFromResponse(result); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - try { - await axios({ - url: "/session/verify", - baseURL: "http://localhost:9876", - method: "post", - }); - } catch (err) { - if (err !== undefined && err.response !== undefined) { - assert.strictEqual(err.response.status, 401); - assert.deepStrictEqual(err.response.data, { message: "unauthorised" }); - } else { - throw err; - } - } - - try { - await axios({ - url: "/session/verify", - baseURL: "http://localhost:9876", - method: "post", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - }, - }); - } catch (err) { - if (err !== undefined && err.response !== undefined) { - assert.strictEqual(err.response.status, 401); - assert.deepStrictEqual(err.response.data, { message: "try refresh token" }); - } else { - throw err; - } - } - - result = await axios({ - url: "/session/verify", - baseURL: "http://localhost:9876", - method: "post", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - assert.deepStrictEqual(result.data, { user: "userId" }); - - result = await axios({ - url: "/session/verify/optionalCSRF", - baseURL: "http://localhost:9876", - method: "post", - headers: { - Cookie: `sAccessToken=${res.accessToken}`, - }, - }); - assert.deepStrictEqual(result.data, { user: "userId" }); - - try { - await axios({ - url: "/auth/session/refresh", - baseURL: "http://localhost:9876", - method: "post", - }); - } catch (err) { - if (err !== undefined && err.response !== undefined) { - assert.strictEqual(err.response.status, 401); - assert.deepStrictEqual(err.response.data, { message: "unauthorised" }); - } else { - throw err; - } - } - - result = await axios({ - url: "/auth/session/refresh", - baseURL: "http://localhost:9876", - method: "post", - headers: { - Cookie: `sRefreshToken=${res.refreshToken}`, - "anti-csrf": res.antiCsrf, - }, - }); - - let res2 = extractInfoFromResponse(result); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - result = await axios({ - url: "/session/verify", - baseURL: "http://localhost:9876", - method: "post", - headers: { - Cookie: `sAccessToken=${res2.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - assert.deepStrictEqual(result.data, { user: "userId" }); - - let res3 = extractInfoFromResponse(result); - assert(res3.accessToken !== undefined); - - result = await axios({ - url: "/session/revoke", - baseURL: "http://localhost:9876", - method: "post", - headers: { - Cookie: `sAccessToken=${res3.accessToken}`, - "anti-csrf": res2.antiCsrf, - }, - }); - - let sessionRevokedResponseExtracted = extractInfoFromResponse(result); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("sending custom response", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailExistsGET: async function (input) { - input.options.res.setStatusCode(203); - input.options.res.sendJSONResponse({ - custom: true, - }); - return oI.emailExistsGET(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - await this.app.start(); - - let result = await axios({ - url: "/auth/signup/email/exists?email=test@example.com", - baseURL: "http://localhost:9876", - method: "get", - }); - await new Promise((r) => setTimeout(r, 1000)); // we delay so that the API call finishes and doesn't shut the core before the test finishes. - assert(result.status === 203); - assert(result.data.custom); - }); - - it("test that authorization header is read correctly in dashboard recipe", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - if (authHeader === "Bearer testapikey") { - return true; - } - - return false; - }, - }; - }, - }, - }), - ], - }); - - await this.app.start(); - - let result = await axios({ - url: "/auth/dashboard/api/users/count", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(result.status === 200); - }); - - it("test that tags request respond with correct tags", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - let result = await axios({ - url: "/auth/dashboard/api/search/tags", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - }); - - assert(result.status === 200); - assert(result.data.tags.length !== 0); - }); - - it("test that search results correct output for 'email: t'", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - await createUsers(EmailPassword); - - let result = await axios({ - url: "/auth/dashboard/api/users", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - params: { - limit: 10, - email: "t", - }, - }); - assert(result.status === 200); - assert(result.data.users.length === 5); - }); - - it("test that search results correct output for multiple search items", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - await createUsers(EmailPassword); - - let result = await axios({ - url: "/auth/dashboard/api/users", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - params: { - limit: 10, - email: "iresh;john", - }, - }); - - assert(result.status === 200); - assert(result.data.users.length === 1); - }); - - it("test that search results correct output for 'email: iresh'", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - EmailPassword.init(), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - await createUsers(EmailPassword); - - let result = await axios({ - url: "/auth/dashboard/api/users", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - params: { - limit: 10, - email: "iresh;", - }, - }); - - assert(result.status === 200); - assert(result.data.users.length === 0); - }); - - it("test that search results correct output for 'phone: +1'", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - await createUsers(null, Passwordless); - - let result = await axios({ - url: "/auth/dashboard/api/users", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - params: { - limit: 10, - phone: "+1", - }, - }); - - assert(result.status === 200); - assert(result.data.users.length === 3); - }); - - it("test that search results correct output for 'phone: 1('", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - await createUsers(null, Passwordless); - - let result = await axios({ - url: "/auth/dashboard/api/users", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - params: { - limit: 10, - phone: "1(", - }, - }); - - assert(result.status === 200); - assert(result.data.users.length === 0); - }); - - it("test that search results correct output for 'provider: google', phone: 1", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - await createUsers(null, Passwordless, ThirdParty); - - let result = await axios({ - url: "/auth/dashboard/api/users", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - params: { - limit: 10, - provider: "google", - phone: "1", - }, - }); - - assert(result.status === 200); - assert(result.data.users.length === 0); - }); - - it("test that search results correct output for 'provider: google'", async function () { - await startST(); - SuperTokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Dashboard.init({ - apiKey: "testapikey", - override: { - functions: (original) => { - return { - ...original, - shouldAllowAccess: async function (input) { - let authHeader = input.req.getHeaderValue("authorization"); - return authHeader === "Bearer testapikey"; - }, - }; - }, - }, - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Google({ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }), - Github({ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }), - Apple({ - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }, - }), - ], - }); - - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.19") === "2.19") { - return this.skip(); - } - - await this.app.start(); - - await createUsers(null, null, ThirdParty); - - let result = await axios({ - url: "/auth/dashboard/api/users", - baseURL: "http://localhost:9876", - method: "get", - headers: { - Authorization: "Bearer testapikey", - "Content-Type": "application/json", - }, - params: { - limit: 10, - provider: "google", - }, - }); - - assert(result.status === 200); - assert(result.data.users.length === 3); - }); -}); diff --git a/test/framework/loopback.test.ts b/test/framework/loopback.test.ts new file mode 100644 index 000000000..6f9fe73c0 --- /dev/null +++ b/test/framework/loopback.test.ts @@ -0,0 +1,819 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import SuperTokens from 'supertokens-node' +import ProcessState from 'supertokens-node/processState' +import Session from 'supertokens-node/recipe/session' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import axios from 'axios' +import Dashboard from 'supertokens-node/recipe/dashboard' +import { afterEach, beforeEach, describe, it } from 'vitest' +import { RestApplication } from '@loopback/rest' +import { cleanST, extractInfoFromResponse, killAllST, printPath, setupST, startST } from '../utils' +import { app } from './loopback-server' + +import { Apple, Github, Google } from 'supertokens-node/recipe/thirdparty' +import { createUsers } from '../utils' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' + +import ThirdParty from 'supertokens-node/recipe/thirdparty' +import Passwordless from 'supertokens-node/recipe/passwordless' + +describe(`Loopback: ${printPath('[test/framework/loopback.test.ts]')}`, () => { + let server: RestApplication + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + server = app + }) + + afterEach(async () => { + if (server !== undefined) + await server.stop() + }) + + afterEach(async () => { + await killAllST() + await cleanST() + }) + + // check basic usage of session + it('test basic usage of sessions', async () => { + await startST() + SuperTokens.init({ + framework: 'loopback', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + await server.start() + + let result = await axios({ + url: '/create', + baseURL: 'http://localhost:9876', + method: 'post', + }) + const res = extractInfoFromResponse(result) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + try { + await axios({ + url: '/session/verify', + baseURL: 'http://localhost:9876', + method: 'post', + }) + } + catch (err) { + if (err !== undefined && err.response !== undefined) { + assert.strictEqual(err.response.status, 401) + assert.deepStrictEqual(err.response.data, { message: 'unauthorised' }) + } + else { + throw err + } + } + + try { + await axios({ + url: '/session/verify', + baseURL: 'http://localhost:9876', + method: 'post', + headers: { + Cookie: `sAccessToken=${res.accessToken}`, + }, + }) + } + catch (err) { + if (err !== undefined && err.response !== undefined) { + assert.strictEqual(err.response.status, 401) + assert.deepStrictEqual(err.response.data, { message: 'try refresh token' }) + } + else { + throw err + } + } + + result = await axios({ + url: '/session/verify', + baseURL: 'http://localhost:9876', + method: 'post', + headers: { + 'Cookie': `sAccessToken=${res.accessToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + assert.deepStrictEqual(result.data, { user: 'userId' }) + + result = await axios({ + url: '/session/verify/optionalCSRF', + baseURL: 'http://localhost:9876', + method: 'post', + headers: { + Cookie: `sAccessToken=${res.accessToken}`, + }, + }) + assert.deepStrictEqual(result.data, { user: 'userId' }) + + try { + await axios({ + url: '/auth/session/refresh', + baseURL: 'http://localhost:9876', + method: 'post', + }) + } + catch (err) { + if (err !== undefined && err.response !== undefined) { + assert.strictEqual(err.response.status, 401) + assert.deepStrictEqual(err.response.data, { message: 'unauthorised' }) + } + else { + throw err + } + } + + result = await axios({ + url: '/auth/session/refresh', + baseURL: 'http://localhost:9876', + method: 'post', + headers: { + 'Cookie': `sRefreshToken=${res.refreshToken}`, + 'anti-csrf': res.antiCsrf, + }, + }) + + const res2 = extractInfoFromResponse(result) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + result = await axios({ + url: '/session/verify', + baseURL: 'http://localhost:9876', + method: 'post', + headers: { + 'Cookie': `sAccessToken=${res2.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + assert.deepStrictEqual(result.data, { user: 'userId' }) + + const res3 = extractInfoFromResponse(result) + assert(res3.accessToken !== undefined) + + result = await axios({ + url: '/session/revoke', + baseURL: 'http://localhost:9876', + method: 'post', + headers: { + 'Cookie': `sAccessToken=${res3.accessToken}`, + 'anti-csrf': res2.antiCsrf, + }, + }) + + const sessionRevokedResponseExtracted = extractInfoFromResponse(result) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('sending custom response', async () => { + await startST() + SuperTokens.init({ + framework: 'loopback', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + async emailExistsGET(input) { + input.options.res.setStatusCode(203) + input.options.res.sendJSONResponse({ + custom: true, + }) + return oI.emailExistsGET(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + await server.start() + + const result = await axios({ + url: '/auth/signup/email/exists?email=test@example.com', + baseURL: 'http://localhost:9876', + method: 'get', + }) + await new Promise(r => setTimeout(r, 1000)) // we delay so that the API call finishes and doesn't shut the core before the test finishes. + assert(result.status === 203) + assert(result.data.custom) + }) + + it('test that authorization header is read correctly in dashboard recipe', async () => { + await startST() + SuperTokens.init({ + framework: 'loopback', + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Dashboard.init({ + apiKey: 'testapikey', + override: { + functions: (original) => { + return { + ...original, + async shouldAllowAccess(input) { + const authHeader = input.req.getHeaderValue('authorization') + if (authHeader === 'Bearer testapikey') + return true + + return false + }, + } + }, + }, + }), + ], + }) + + await server.start() + + const result = await axios({ + url: '/auth/dashboard/api/users/count', + baseURL: 'http://localhost:9876', + method: 'get', + headers: { + 'Authorization': 'Bearer testapikey', + 'Content-Type': 'application/json', + }, + }) + + assert(result.status === 200) + }) + + + + it("test that tags request respond with correct tags", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + let result = await axios({ + url: "/auth/dashboard/api/search/tags", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + }); + + assert(result.status === 200); + assert(result.data.tags.length !== 0); + }); + + it("test that search results correct output for 'email: t'", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + await createUsers(EmailPassword); + + let result = await axios({ + url: "/auth/dashboard/api/users", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + params: { + limit: 10, + email: "t", + }, + }); + assert(result.status === 200); + assert(result.data.users.length === 5); + }); + + it("test that search results correct output for multiple search items", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + await createUsers(EmailPassword); + + let result = await axios({ + url: "/auth/dashboard/api/users", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + params: { + limit: 10, + email: "iresh;john", + }, + }); + + assert(result.status === 200); + assert(result.data.users.length === 1); + }); + + it("test that search results correct output for 'email: iresh'", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + EmailPassword.init(), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + await createUsers(EmailPassword); + + let result = await axios({ + url: "/auth/dashboard/api/users", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + params: { + limit: 10, + email: "iresh;", + }, + }); + + assert(result.status === 200); + assert(result.data.users.length === 0); + }); + + it("test that search results correct output for 'phone: +1'", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + await createUsers(null, Passwordless); + + let result = await axios({ + url: "/auth/dashboard/api/users", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + params: { + limit: 10, + phone: "+1", + }, + }); + + assert(result.status === 200); + assert(result.data.users.length === 3); + }); + + it("test that search results correct output for 'phone: 1('", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + await createUsers(null, Passwordless); + + let result = await axios({ + url: "/auth/dashboard/api/users", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + params: { + limit: 10, + phone: "1(", + }, + }); + + assert(result.status === 200); + assert(result.data.users.length === 0); + }); + + it("test that search results correct output for 'provider: google', phone: 1", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + await createUsers(null, Passwordless, ThirdParty); + + let result = await axios({ + url: "/auth/dashboard/api/users", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + params: { + limit: 10, + provider: "google", + phone: "1", + }, + }); + + assert(result.status === 200); + assert(result.data.users.length === 0); + }); + + it("test that search results correct output for 'provider: google'", async function () { + await startST(); + SuperTokens.init({ + framework: "loopback", + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Dashboard.init({ + apiKey: "testapikey", + override: { + functions: (original) => { + return { + ...original, + shouldAllowAccess: async function (input) { + let authHeader = input.req.getHeaderValue("authorization"); + return authHeader === "Bearer testapikey"; + }, + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Google({ + clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", + clientSecret: "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", + }), + Github({ + clientId: "467101b197249757c71f", + clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", + }), + Apple({ + clientId: "4398792-io.supertokens.example.service", + clientSecret: { + keyId: "7M48Y4RYDL", + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + teamId: "YWQCXGJRJL", + }, + }), + ], + }, + }), + ], + }); + + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.19") === "2.19") { + return this.skip(); + } + + await this.app.start(); + + await createUsers(null, null, ThirdParty); + + let result = await axios({ + url: "/auth/dashboard/api/users", + baseURL: "http://localhost:9876", + method: "get", + headers: { + Authorization: "Bearer testapikey", + "Content-Type": "application/json", + }, + params: { + limit: 10, + provider: "google", + }, + }); + + assert(result.status === 200); + assert(result.data.users.length === 3); + }); +}) diff --git a/test/handshake.test.js b/test/handshake.test.js deleted file mode 100644 index bf9db8e5a..000000000 --- a/test/handshake.test.js +++ /dev/null @@ -1,151 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST } = require("./utils"); -let ST = require("../"); -let Session = require("../recipe/session"); -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); -const { maxVersion } = require("../lib/build/utils"); - -describe(`Handshake: ${printPath("[test/handshake.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // test that once the info is loaded, it doesn't query again - it("test that once the info is loaded, it doesn't querry again", async function () { - await startST(); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let sessionRecipeInstance = SessionRecipe.getInstanceOrThrowError(); - await sessionRecipeInstance.recipeInterfaceImpl.getHandshakeInfo(); - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO, - 2000 - ); - assert(verifyState !== undefined); - - ProcessState.getInstance().reset(); - - await sessionRecipeInstance.recipeInterfaceImpl.getHandshakeInfo(); - verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO, - 2000 - ); - assert(verifyState === undefined); - }); - - it("core not available", async function () { - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - try { - await Session.revokeSession(""); - throw new Error("should not have come here"); - } catch (err) { - if (err.message !== "No SuperTokens core available to query") { - throw err; - } - } - }); - - it("successful handshake and update JWT without keyList", async function () { - await startST(); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - let info = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo(); - assert(info.getJwtSigningPublicKeyList() instanceof Array); - assert.equal(info.getJwtSigningPublicKeyList().length, 1); - assert.strictEqual(info.antiCsrf, "NONE"); - assert.equal(info.accessTokenBlacklistingEnabled, false); - assert.equal(info.accessTokenValidity, 3600 * 1000); - assert.equal(info.refreshTokenValidity, 144000 * 60 * 1000); - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.updateJwtSigningPublicKeyInfo( - undefined, - "hello", - Date.now() + 1000 - ); - let info2 = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo(); - assert.equal(info2.getJwtSigningPublicKeyList().length, 1); - assert.equal(info2.getJwtSigningPublicKeyList()[0].publicKey, "hello"); - assert(info2.getJwtSigningPublicKeyList()[0].expiryTime <= Date.now() + 1000); - assert(info2.getJwtSigningPublicKeyList()[0].createdAt >= Date.now() - 1000); - }); - - it("successful handshake and update JWT with keyList", async function () { - await startST(); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - let info = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo(); - assert(info.getJwtSigningPublicKeyList() instanceof Array); - assert.equal(info.getJwtSigningPublicKeyList().length, 1); - assert.strictEqual(info.antiCsrf, "NONE"); - assert.equal(info.accessTokenBlacklistingEnabled, false); - assert.equal(info.accessTokenValidity, 3600 * 1000); - assert.equal(info.refreshTokenValidity, 144000 * 60 * 1000); - const expiryTime = Date.now() + 1000; - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.updateJwtSigningPublicKeyInfo( - [{ publicKey: "hello2", expiryTime }], - "hello2", - expiryTime - ); - let info2 = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo(); - assert.deepStrictEqual(info2.getJwtSigningPublicKeyList(), [{ publicKey: "hello2", expiryTime }]); - }); -}); diff --git a/test/handshake.test.ts b/test/handshake.test.ts new file mode 100644 index 000000000..9131e742a --- /dev/null +++ b/test/handshake.test.ts @@ -0,0 +1,151 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import assert from 'assert' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import Session from 'supertokens-node/recipe/session' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import ST from 'supertokens-node' +import { cleanST, killAllST, printPath, setupST, startST } from './utils' + +describe(`Handshake: ${printPath('[test/handshake.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // test that once the info is loaded, it doesn't query again + it('test that once the info is loaded, it doesn\'t querry again', async () => { + await startST() + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const sessionRecipeInstance = SessionRecipe.getInstanceOrThrowError() + await sessionRecipeInstance.recipeInterfaceImpl.getHandshakeInfo() + let verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO, + 2000, + ) + assert(verifyState !== undefined) + + ProcessState.getInstance().reset() + + await sessionRecipeInstance.recipeInterfaceImpl.getHandshakeInfo() + verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO, + 2000, + ) + assert(verifyState === undefined) + }) + + it('core not available', async () => { + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + try { + await Session.revokeSession('') + throw new Error('should not have come here') + } + catch (err) { + if (err.message !== 'No SuperTokens core available to query') + throw err + } + }) + + it('successful handshake and update JWT without keyList', async () => { + await startST() + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const info = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() + assert(Array.isArray(info.getJwtSigningPublicKeyList())) + assert.equal(info.getJwtSigningPublicKeyList().length, 1) + assert.strictEqual(info.antiCsrf, 'NONE') + assert.equal(info.accessTokenBlacklistingEnabled, false) + assert.equal(info.accessTokenValidity, 3600 * 1000) + assert.equal(info.refreshTokenValidity, 144000 * 60 * 1000) + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.updateJwtSigningPublicKeyInfo( + undefined, + 'hello', + Date.now() + 1000, + ) + const info2 = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() + assert.equal(info2.getJwtSigningPublicKeyList().length, 1) + assert.equal(info2.getJwtSigningPublicKeyList()[0].publicKey, 'hello') + assert(info2.getJwtSigningPublicKeyList()[0].expiryTime <= Date.now() + 1000) + assert(info2.getJwtSigningPublicKeyList()[0].createdAt >= Date.now() - 1000) + }) + + it('successful handshake and update JWT with keyList', async () => { + await startST() + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + const info = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() + assert(Array.isArray(info.getJwtSigningPublicKeyList())) + assert.equal(info.getJwtSigningPublicKeyList().length, 1) + assert.strictEqual(info.antiCsrf, 'NONE') + assert.equal(info.accessTokenBlacklistingEnabled, false) + assert.equal(info.accessTokenValidity, 3600 * 1000) + assert.equal(info.refreshTokenValidity, 144000 * 60 * 1000) + const expiryTime = Date.now() + 1000 + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.updateJwtSigningPublicKeyInfo( + [{ publicKey: 'hello2', expiryTime }], + 'hello2', + expiryTime, + ) + const info2 = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() + assert.deepStrictEqual(info2.getJwtSigningPublicKeyList(), [{ publicKey: 'hello2', expiryTime }]) + }) +}) diff --git a/test/humanise.test.js b/test/humanise.test.js deleted file mode 100644 index 72e1cb80b..000000000 --- a/test/humanise.test.js +++ /dev/null @@ -1,32 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath } = require("./utils"); -const { humaniseMilliseconds } = require("../lib/build/utils"); -const assert = require("assert"); - -describe(`Humanise: ${printPath("[test/humanise.test.js]")}`, function () { - it("test humanise milliseconds", function () { - assert("1 second" === humaniseMilliseconds(1000)); - assert("59 seconds" === humaniseMilliseconds(59000)); - assert("1 minute" === humaniseMilliseconds(60000)); - assert("1 minute" === humaniseMilliseconds(119000)); - assert("2 minutes" === humaniseMilliseconds(120000)); - assert("1 hour" === humaniseMilliseconds(3600000)); - assert("1 hour" === humaniseMilliseconds(3660000)); - assert("1.1 hours" === humaniseMilliseconds(3960000)); - assert("2 hours" === humaniseMilliseconds(7260000)); - assert("5 hours" === humaniseMilliseconds(18000000)); - }); -}); diff --git a/test/humanise.test.ts b/test/humanise.test.ts new file mode 100644 index 000000000..379af7e23 --- /dev/null +++ b/test/humanise.test.ts @@ -0,0 +1,34 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { humaniseMilliseconds } from 'supertokens-node/utils' +import { describe, it } from 'vitest' +import { printPath } from './utils' + +describe(`Humanise: ${printPath('[test/humanise.test.ts]')}`, () => { + it('test humanise milliseconds', () => { + assert(humaniseMilliseconds(1000) === '1 second') + assert(humaniseMilliseconds(59000) === '59 seconds') + assert(humaniseMilliseconds(60000) === '1 minute') + assert(humaniseMilliseconds(119000) === '1 minute') + assert(humaniseMilliseconds(120000) === '2 minutes') + assert(humaniseMilliseconds(3600000) === '1 hour') + assert(humaniseMilliseconds(3660000) === '1 hour') + assert(humaniseMilliseconds(3960000) === '1.1 hours') + assert(humaniseMilliseconds(7260000) === '2 hours') + assert(humaniseMilliseconds(18000000) === '5 hours') + }) +}) diff --git a/test/import.test.js b/test/import.test.js deleted file mode 100644 index cc1997f92..000000000 --- a/test/import.test.js +++ /dev/null @@ -1,43 +0,0 @@ -const { printPath, getAllFilesInDirectory } = require("./utils"); -const { resolve } = require("path"); -const { writeFileSync, rmSync } = require("fs"); -const { existsSync } = require("fs"); -const { execSync } = require("child_process"); - -const testFileName = "importtest.js"; -const testFilePath = resolve(process.cwd(), `./${testFileName}`); - -describe(`importTests: ${printPath("[test/import.test.js]")}`, function () { - after(function () { - // The exists check is just a precaution - if (existsSync(testFilePath)) { - rmSync(testFilePath); - } - }); - - /** - * This test does the following: - * 1. Gets a list of all files in the build folder recursively - * 2. For each build file, creates a simple js file that imports the build file - * 3. Runs the generated js file - * - * The test fails if there is any error thrown when trying to run any of the generated files. - * - * This is to prevent issues arising from circular imports where certain variables from the - * default exports for recipes are not intialised correctly. - * (Refer to: https://github.com/supertokens/supertokens-node/issues/513) - */ - it("Test that importing all build files independently does not cause errors", function () { - const fileNames = getAllFilesInDirectory(resolve(process.cwd(), "./lib/build")).filter( - (i) => !i.endsWith(".d.ts") - ); - - fileNames.forEach((fileName) => { - const relativeFilePath = fileName.replace(process.cwd(), ""); - writeFileSync(testFilePath, `require(".${relativeFilePath}")`); - - // This will throw an error if the command fails - execSync(`node ${resolve(process.cwd(), `./${testFileName}`)}`); - }); - }); -}); diff --git a/test/jwt/config.test.js b/test/jwt/config.test.js deleted file mode 100644 index c07ffdbdc..000000000 --- a/test/jwt/config.test.js +++ /dev/null @@ -1,75 +0,0 @@ -let assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -const JWTRecipe = require("../../lib/build/recipe/jwt/recipe").default; -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`configTest: ${printPath("[test/jwt/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that the default config sets values correctly for JWT recipe", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [JWTRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let jwtRecipe = await JWTRecipe.getInstanceOrThrowError(); - assert(jwtRecipe.config.jwtValiditySeconds === 3153600000); - }); - - it("Test that the config sets values correctly for JWT recipe when jwt validity is set", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - JWTRecipe.init({ - jwtValiditySeconds: 24 * 60 * 60, // 24 hours - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let jwtRecipe = await JWTRecipe.getInstanceOrThrowError(); - assert(jwtRecipe.config.jwtValiditySeconds === 24 * 60 * 60); - }); -}); diff --git a/test/jwt/config.test.ts b/test/jwt/config.test.ts new file mode 100644 index 000000000..8d1c3c7d6 --- /dev/null +++ b/test/jwt/config.test.ts @@ -0,0 +1,73 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import { Querier } from 'supertokens-node/querier' +import JWTRecipe from 'supertokens-node/recipe/jwt/recipe' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`configTest: ${printPath('[test/jwt/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that the default config sets values correctly for JWT recipe', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [JWTRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const jwtRecipe = await JWTRecipe.getInstanceOrThrowError() + assert(jwtRecipe.config.jwtValiditySeconds === 3153600000) + }) + + it('Test that the config sets values correctly for JWT recipe when jwt validity is set', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + JWTRecipe.init({ + jwtValiditySeconds: 24 * 60 * 60, // 24 hours + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const jwtRecipe = await JWTRecipe.getInstanceOrThrowError() + assert(jwtRecipe.config.jwtValiditySeconds === 24 * 60 * 60) + }) +}) diff --git a/test/jwt/createJWTFeature.test.js b/test/jwt/createJWTFeature.test.js deleted file mode 100644 index 318013626..000000000 --- a/test/jwt/createJWTFeature.test.js +++ /dev/null @@ -1,197 +0,0 @@ -let assert = require("assert"); -const e = require("cors"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let JWTRecipe = require("../../lib/build/recipe/jwt"); -let { ProcessState } = require("../../lib/build/processState"); -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`createJWTFeature: ${printPath("[test/jwt/createJWTFeature.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that sending 0 validity throws an error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [JWTRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - try { - await JWTRecipe.createJWT({}, 0); - assert.fail(); - } catch (ignored) { - // TODO (During Review): Should we check for the error message? - } - }); - - it("Test that sending a invalid json throws an error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [JWTRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let jwt = undefined; - - try { - jwt = await JWTRecipe.createJWT("invalidjson", 1000); - } catch (err) { - // TODO (During Review): Should we check for the error message? - } - - assert(jwt === undefined); - }); - - it("Test that returned JWT uses 100 years for expiry for default config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [JWTRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let currentTimeInSeconds = Date.now() / 1000; - let jwt = (await JWTRecipe.createJWT({})).jwt.split(".")[1]; - let decodedJWTPayload = Buffer.from(jwt, "base64").toString("utf-8"); - - let targetExpiryDuration = 3153600000; // 100 years in seconds - let jwtExpiry = JSON.parse(decodedJWTPayload)["exp"]; - let actualExpiry = jwtExpiry - currentTimeInSeconds; - - let differenceInExpiryDurations = Math.abs(actualExpiry - targetExpiryDuration); - - // Both expiry durations should be within 5 seconds of each other. Using 5 seconds as a worst case buffer - assert(differenceInExpiryDurations < 5); - }); - - it("Test that jwt validity is same as validity set in config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - JWTRecipe.init({ - jwtValiditySeconds: 1000, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let currentTimeInSeconds = Date.now() / 1000; - let jwt = (await JWTRecipe.createJWT({})).jwt.split(".")[1]; - let decodedJWTPayload = Buffer.from(jwt, "base64").toString("utf-8"); - - let targetExpiryDuration = 1000; // 100 years in seconds - let jwtExpiry = JSON.parse(decodedJWTPayload)["exp"]; - let actualExpiry = jwtExpiry - currentTimeInSeconds; - - let differenceInExpiryDurations = Math.abs(actualExpiry - targetExpiryDuration); - - // Both expiry durations should be within 5 seconds of each other. Using 5 seconds as a worst case buffer - assert(differenceInExpiryDurations < 5); - }); - - it("Test that jwt validity is same as validity passed in createJWT function", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - JWTRecipe.init({ - jwtValiditySeconds: 1000, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let currentTimeInSeconds = Date.now() / 1000; - let targetExpiryDuration = 500; // 100 years in seconds - - let jwt = (await JWTRecipe.createJWT({}, targetExpiryDuration)).jwt.split(".")[1]; - let decodedJWTPayload = Buffer.from(jwt, "base64").toString("utf-8"); - - let jwtExpiry = JSON.parse(decodedJWTPayload)["exp"]; - let actualExpiry = jwtExpiry - currentTimeInSeconds; - - let differenceInExpiryDurations = Math.abs(actualExpiry - targetExpiryDuration); - - // Both expiry durations should be within 5 seconds of each other. Using 5 seconds as a worst case buffer - assert(differenceInExpiryDurations < 5); - }); -}); diff --git a/test/jwt/createJWTFeature.test.ts b/test/jwt/createJWTFeature.test.ts new file mode 100644 index 000000000..a97e7b7c7 --- /dev/null +++ b/test/jwt/createJWTFeature.test.ts @@ -0,0 +1,194 @@ +import assert from 'assert' + +import STExpress from 'supertokens-node' +import JWTRecipe from 'supertokens-node/recipe/jwt' +import { ProcessState } from 'supertokens-node/processState' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`createJWTFeature: ${printPath('[test/jwt/createJWTFeature.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that sending 0 validity throws an error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [JWTRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + try { + await JWTRecipe.createJWT({}, 0) + assert.fail() + } + catch (ignored) { + // TODO (During Review): Should we check for the error message? + } + }) + + it('Test that sending a invalid json throws an error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [JWTRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + let jwt + + try { + jwt = await JWTRecipe.createJWT('invalidjson', 1000) + } + catch (err) { + // TODO (During Review): Should we check for the error message? + } + + assert(jwt === undefined) + }) + + it('Test that returned JWT uses 100 years for expiry for default config', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [JWTRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const currentTimeInSeconds = Date.now() / 1000 + const jwt = (await JWTRecipe.createJWT({})).jwt.split('.')[1] + const decodedJWTPayload = Buffer.from(jwt, 'base64').toString('utf-8') + + const targetExpiryDuration = 3153600000 // 100 years in seconds + const jwtExpiry = JSON.parse(decodedJWTPayload).exp + const actualExpiry = jwtExpiry - currentTimeInSeconds + + const differenceInExpiryDurations = Math.abs(actualExpiry - targetExpiryDuration) + + // Both expiry durations should be within 5 seconds of each other. Using 5 seconds as a worst case buffer + assert(differenceInExpiryDurations < 5) + }) + + it('Test that jwt validity is same as validity set in config', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + JWTRecipe.init({ + jwtValiditySeconds: 1000, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const currentTimeInSeconds = Date.now() / 1000 + const jwt = (await JWTRecipe.createJWT({})).jwt.split('.')[1] + const decodedJWTPayload = Buffer.from(jwt, 'base64').toString('utf-8') + + const targetExpiryDuration = 1000 // 100 years in seconds + const jwtExpiry = JSON.parse(decodedJWTPayload).exp + const actualExpiry = jwtExpiry - currentTimeInSeconds + + const differenceInExpiryDurations = Math.abs(actualExpiry - targetExpiryDuration) + + // Both expiry durations should be within 5 seconds of each other. Using 5 seconds as a worst case buffer + assert(differenceInExpiryDurations < 5) + }) + + it('Test that jwt validity is same as validity passed in createJWT function', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + JWTRecipe.init({ + jwtValiditySeconds: 1000, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const currentTimeInSeconds = Date.now() / 1000 + const targetExpiryDuration = 500 // 100 years in seconds + + const jwt = (await JWTRecipe.createJWT({}, targetExpiryDuration)).jwt.split('.')[1] + const decodedJWTPayload = Buffer.from(jwt, 'base64').toString('utf-8') + + const jwtExpiry = JSON.parse(decodedJWTPayload).exp + const actualExpiry = jwtExpiry - currentTimeInSeconds + + const differenceInExpiryDurations = Math.abs(actualExpiry - targetExpiryDuration) + + // Both expiry durations should be within 5 seconds of each other. Using 5 seconds as a worst case buffer + assert(differenceInExpiryDurations < 5) + }) +}) diff --git a/test/jwt/getJWKS.test.js b/test/jwt/getJWKS.test.js deleted file mode 100644 index 6d7ee8660..000000000 --- a/test/jwt/getJWKS.test.js +++ /dev/null @@ -1,121 +0,0 @@ -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let { ProcessState } = require("../../lib/build/processState"); -let JWTRecipe = require("../../lib/build/recipe/jwt"); -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that default getJWKS api does not work when disabled", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - JWTRecipe.init({ - override: { - apis: async (originalImplementation) => { - return { - ...originalImplementation, - getJWKSGET: undefined, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => { - request(app) - .get("/auth/jwt/jwks.json") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); - - assert(response.status === 404); - }); - - it("Test that default getJWKS works fine", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [JWTRecipe.init({})], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => { - request(app) - .get("/auth/jwt/jwks.json") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }); - }); - - assert(response !== undefined); - assert(response.keys !== undefined); - assert(response.keys.length > 0); - }); -}); diff --git a/test/jwt/getJWKS.test.ts b/test/jwt/getJWKS.test.ts new file mode 100644 index 000000000..a39d785e2 --- /dev/null +++ b/test/jwt/getJWKS.test.ts @@ -0,0 +1,120 @@ +import assert from 'assert' +import express from 'express' +import request from 'supertest' + +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import { Querier } from 'supertokens-node/querier' +import JWTRecipe from 'supertokens-node/recipe/jwt/recipe' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getJWKS: ${printPath('[test/jwt/getJWKS.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that default getJWKS api does not work when disabled', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + JWTRecipe.init({ + override: { + apis: async (originalImplementation) => { + return { + ...originalImplementation, + getJWKSGET: undefined, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise((resolve) => { + request(app) + .get('/auth/jwt/jwks.json') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) + + assert(response.status === 404) + }) + + it('Test that default getJWKS works fine', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [JWTRecipe.init({})], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise((resolve) => { + request(app) + .get('/auth/jwt/jwks.json') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }) + }) + + assert(response !== undefined) + assert(response.keys !== undefined) + assert(response.keys.length > 0) + }) +}) diff --git a/test/jwt/override.test.js b/test/jwt/override.test.js deleted file mode 100644 index c98279a82..000000000 --- a/test/jwt/override.test.js +++ /dev/null @@ -1,184 +0,0 @@ -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let { ProcessState } = require("../../lib/build/processState"); -let JWTRecipe = require("../../lib/build/recipe/jwt"); -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`overrideTest: ${printPath("[test/jwt/override.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test overriding functions", async function () { - await startST(); - - let jwtCreated = undefined; - let jwksKeys = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - JWTRecipe.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - createJWT: async (input) => { - let createJWTResponse = await originalImplementation.createJWT(input); - - if (createJWTResponse.status === "OK") { - jwtCreated = createJWTResponse.jwt; - } - - return createJWTResponse; - }, - getJWKS: async () => { - let getJWKSResponse = await originalImplementation.getJWKS(); - - if (getJWKSResponse.status === "OK") { - jwksKeys = getJWKSResponse.keys; - } - - return getJWKSResponse; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - app.use(express.json()); - - app.post("/jwtcreate", async (req, res) => { - let payload = req.body.payload; - res.json(await JWTRecipe.createJWT(payload, 1000)); - }); - - let createJWTResponse = await new Promise((resolve) => { - request(app) - .post("/jwtcreate") - .send({ - payload: { someKey: "someValue" }, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }); - }); - - assert.notStrictEqual(jwtCreated, undefined); - assert.deepStrictEqual(jwtCreated, createJWTResponse.jwt); - - let getJWKSResponse = await new Promise((resolve) => { - request(app) - .get("/auth/jwt/jwks.json") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }); - }); - - assert.notStrictEqual(jwksKeys, undefined); - assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys); - }); - - it("Test overriding APIs", async function () { - await startST(); - - let jwksKeys = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - JWTRecipe.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - getJWKSGET: async (input) => { - let response = await originalImplementation.getJWKSGET(input); - jwksKeys = response.keys; - return response; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let getJWKSResponse = await new Promise((resolve) => { - request(app) - .get("/auth/jwt/jwks.json") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }); - }); - - assert.notStrictEqual(jwksKeys, undefined); - assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys); - }); -}); diff --git a/test/jwt/override.test.ts b/test/jwt/override.test.ts new file mode 100644 index 000000000..0694a7a4a --- /dev/null +++ b/test/jwt/override.test.ts @@ -0,0 +1,181 @@ +import assert from 'assert' +import express from 'express' +import request from 'supertest' + +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import JWTRecipe from 'supertokens-node/recipe/jwt' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`overrideTest: ${printPath('[test/jwt/override.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test overriding functions', async () => { + await startST() + + let jwtCreated + let jwksKeys + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + JWTRecipe.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + createJWT: async (input) => { + const createJWTResponse = await originalImplementation.createJWT(input) + + if (createJWTResponse.status === 'OK') + jwtCreated = createJWTResponse.jwt + + return createJWTResponse + }, + getJWKS: async () => { + const getJWKSResponse = await originalImplementation.getJWKS() + + if (getJWKSResponse.status === 'OK') + jwksKeys = getJWKSResponse.keys + + return getJWKSResponse + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + app.use(express.json()) + + app.post('/jwtcreate', async (req, res) => { + const payload = req.body.payload + res.json(await JWTRecipe.createJWT(payload, 1000)) + }) + + const createJWTResponse = await new Promise((resolve) => { + request(app) + .post('/jwtcreate') + .send({ + payload: { someKey: 'someValue' }, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }) + }) + + assert.notStrictEqual(jwtCreated, undefined) + assert.deepStrictEqual(jwtCreated, createJWTResponse.jwt) + + const getJWKSResponse = await new Promise((resolve) => { + request(app) + .get('/auth/jwt/jwks.json') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }) + }) + + assert.notStrictEqual(jwksKeys, undefined) + assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys) + }) + + it('Test overriding APIs', async () => { + await startST() + + let jwksKeys + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + JWTRecipe.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + getJWKSGET: async (input) => { + const response = await originalImplementation.getJWKSGET(input) + jwksKeys = response.keys + return response + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const getJWKSResponse = await new Promise((resolve) => { + request(app) + .get('/auth/jwt/jwks.json') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }) + }) + + assert.notStrictEqual(jwksKeys, undefined) + assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys) + }) +}) diff --git a/test/middleware.test.js b/test/middleware.test.js deleted file mode 100644 index c85d52491..000000000 --- a/test/middleware.test.js +++ /dev/null @@ -1,1795 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - setKeyValueInConfig, - extractInfoFromResponse, - delay, -} = require("./utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { Querier } = require("../lib/build/querier"); -let { ProcessState } = require("../lib/build/processState"); -let SuperTokens = require("../"); -let Session = require("../recipe/session"); -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -let { middleware, errorHandler } = require("../framework/express"); -let { verifySession } = require("../recipe/session/framework/express"); - -/** - * TODO: (Later) check that disabling default API actually disables it (for emailpassword) - */ - -describe(`middleware: ${printPath("[test/middleware.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // check that disabling default API actually disables it (for session) - it("test disabling default API actually disables it", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 404); - }); - - it("test session verify middleware", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { - res.setStatusCode(403); - return res.sendJSONResponse({ - message: "token theft detected", - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", {}, {}); - res.status(200).json({ message: true }); - }); - - app.get("/user/id", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getUserId() }); - }); - - app.get("/user/handleV0", verifySession({ antiCsrfCheck: true }), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - - app.get( - "/user/handleV1", - verifySession({ - antiCsrfCheck: true, - }), - async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - } - ); - - app.get( - "/user/handleOptional", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.post("/auth/session/refresh", verifySession(), async (req, res, next) => { - res.status(200).json({ message: true }); - }); - - app.post("/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - - let res1 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let r1 = await new Promise((resolve) => - request(app) - .get("/user/id") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r1 === "testing-userId"); - - await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - // not passing anti csrf even if requried - let r2V0 = await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V0 === "try refresh token"); - - let r2V1 = await new Promise((resolve) => - request(app) - .get("/user/handleV1") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V1 === "try refresh token"); - - let r2Optional = await new Promise((resolve) => - request(app) - .get("/user/handleOptional") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2Optional === true); - - r2Optional = await new Promise((resolve) => - request(app) - .get("/user/handleOptional") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2Optional === false); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .get("/user/id") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - let r4 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(403) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r4 === "token theft detected"); - - let res4 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/logout") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(res4.antiCsrf, undefined); - assert.strictEqual(res4.accessToken, ""); - assert.strictEqual(res4.refreshToken, ""); - assert.strictEqual(res4.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res4.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - - let r5 = await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res4.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert.strictEqual(r5, "unauthorised"); - }); - - it("test session verify middleware with auto refresh", async function () { - await setKeyValueInConfig(2); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { - res.setStatusCode(403); - return res.sendJSONResponse({ - message: "token theft detected", - }); - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", {}, {}); - res.status(200).json({ message: true }); - }); - - app.get("/user/id", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getUserId() }); - }); - - app.get("/user/handleV0", verifySession({ antiCsrfCheck: true }), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - - app.get( - "/user/handleV1", - verifySession({ - antiCsrfCheck: true, - }), - async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - } - ); - - app.get( - "/user/handleOptional", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.post("/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - - let res1 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let r1 = await new Promise((resolve) => - request(app) - .get("/user/id") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r1 === "testing-userId"); - - await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - // not passing anti csrf even if requried - let r2V0 = await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V0 === "try refresh token"); - - let r2V1 = await new Promise((resolve) => - request(app) - .get("/user/handleV1") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V1 === "try refresh token"); - - let rOptionalSession = await new Promise((resolve) => - request(app) - .get("/user/handleOptional") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(rOptionalSession === true); - - rOptionalSession = await new Promise((resolve) => - request(app) - .get("/user/handleOptional") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(rOptionalSession === false); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .get("/user/id") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - let r4 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(403) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - } - }) - ); - assert(r4 === "token theft detected"); - - let res4 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/logout") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(res4.antiCsrf, undefined); - assert.strictEqual(res4.accessToken, ""); - assert.strictEqual(res4.refreshToken, ""); - assert.strictEqual(res4.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res4.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - - await delay(2); - let r5 = await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert.strictEqual(r5, "try refresh token"); - }); - - it("test session verify middleware with driver config", async function () { - await setKeyValueInConfig(2); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/custom", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - cookieDomain: "test-driver", - cookieSecure: true, - cookieSameSite: "strict", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { - res.setStatusCode(403); - return res.sendJSONResponse({ - message: "token theft detected", - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", {}, {}); - res.status(200).json({ message: true }); - }); - - app.get("/custom/user/id", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getUserId() }); - }); - - app.get("/custom/user/handleV0", verifySession({ antiCsrfCheck: true }), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - - app.get( - "/custom/user/handleV1", - verifySession({ - antiCsrfCheck: true, - }), - async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - } - ); - - app.get( - "/custom/user/handleOptional", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.post("/custom/session/refresh", verifySession(), async (req, res, next) => { - res.status(200).json({ message: true }); - }); - - app.post("/custom/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - - let res1 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let r1 = await new Promise((resolve) => - request(app) - .get("/custom/user/id") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - assert(r1 === "testing-userId"); - - await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - // not passing anti csrf even if requried - let r2V0 = await new Promise((resolve) => - request(app) - .get("/custom/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V0 === "try refresh token"); - - let r2V1 = await new Promise((resolve) => - request(app) - .get("/custom/user/handleV1") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V1 === "try refresh token"); - - let rOptionalSession = await new Promise((resolve) => - request(app) - .get("/custom/user/handleOptional") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(rOptionalSession === true); - - rOptionalSession = await new Promise((resolve) => - request(app) - .get("/custom/user/handleOptional") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(rOptionalSession === false); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/custom/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .get("/custom/user/id") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(app) - .get("/custom/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - let r4 = await new Promise((resolve) => - request(app) - .post("/custom/session/refresh") - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(403) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r4 === "token theft detected"); - - let res4 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/custom/logout") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(res4.antiCsrf, undefined); - assert.strictEqual(res4.accessToken, ""); - assert.strictEqual(res4.refreshToken, ""); - assert.strictEqual(res4.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res4.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - await delay(2); - let r5 = await new Promise((resolve) => - request(app) - .get("/custom/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert.strictEqual(r5, "try refresh token"); - }); - - it("test session verify middleware with driver config with auto refresh", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/custom", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - cookieDomain: "test-driver", - cookieSecure: true, - cookieSameSite: "strict", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { - res.setStatusCode(403); - return res.sendJSONResponse({ - message: "token theft detected", - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", {}, {}); - res.status(200).json({ message: true }); - }); - - app.get("/custom/user/id", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getUserId() }); - }); - - app.get("/custom/user/handleV0", verifySession({ antiCsrfCheck: true }), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - - app.get( - "/custom/user/handleV1", - verifySession({ - antiCsrfCheck: true, - }), - async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - } - ); - - app.get( - "/custom/user/handleOptional", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.post("/custom/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - - let res1 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res1.accessTokenHttpOnly); - - assert(res1.refreshTokenHttpOnly); - - let r1 = await new Promise((resolve) => - request(app) - .get("/custom/user/id") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - assert(r1 === "testing-userId"); - - await new Promise((resolve) => - request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - // not passing anti csrf even if requried - let r2V0 = await new Promise((resolve) => - request(app) - .get("/custom/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V0 === "try refresh token"); - - let r2V1 = await new Promise((resolve) => - request(app) - .get("/custom/user/handleV1") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2V1 === "try refresh token"); - - let rOptionalSession = await new Promise((resolve) => - request(app) - .get("/custom/user/handleOptional") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(rOptionalSession === true); - - rOptionalSession = await new Promise((resolve) => - request(app) - .get("/custom/user/handleOptional") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/custom/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .get("/custom/user/id") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(app) - .get("/custom/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - let r4 = await new Promise((resolve) => - request(app) - .post("/custom/session/refresh") - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(403) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r4 === "token theft detected"); - - let res4 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/custom/logout") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(res4.antiCsrf, undefined); - assert.strictEqual(res4.accessToken, ""); - assert.strictEqual(res4.refreshToken, ""); - assert.strictEqual(res4.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res4.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - - await delay(2); - let r5 = await new Promise((resolve) => - request(app) - .get("/custom/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r5 === "try refresh token"); - }); - - // https://github.com/supertokens/supertokens-node/pull/108 - // An expired access token is used and we see that try refresh token error is thrown - it("test session verify middleware with expired access token and session required false", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/custom", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - cookieDomain: "test-driver", - cookieSecure: true, - cookieSameSite: "strict", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { - res.statusCode = 403; - return res.json({ - message: "token theft detected", - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", {}, {}); - res.status(200).json({ message: true }); - }); - - app.get("/custom/user/id", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getUserId() }); - }); - - app.get( - "/custom/user/handle", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.post("/custom/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - - let res1 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res1.accessTokenHttpOnly); - - assert(res1.refreshTokenHttpOnly); - - let r1 = await new Promise((resolve) => - request(app) - .get("/custom/user/id") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - assert(r1 === "testing-userId"); - - await new Promise((resolve) => - request(app) - .get("/user/handle") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - await new Promise((r) => setTimeout(r, 5000)); - - let r2 = await new Promise((resolve) => - request(app) - .get("/custom/user/handle") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2 === "try refresh token"); - }); - - // https://github.com/supertokens/supertokens-node/pull/108 - // A session exists, is refreshed, then is revoked, and then we try and use the access token (after first refresh), and we see that unauthorised error is called. - it("test session verify middleware with old access token and session required false", async function () { - await setKeyValueInConfig("access_token_blacklisting", true); - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/custom", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - cookieDomain: "test-driver", - cookieSecure: true, - cookieSameSite: "strict", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { - res.statusCode = 403; - return res.json({ - message: "token theft detected", - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", {}, {}); - res.status(200).json({ message: true }); - }); - - app.get("/custom/user/id", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getUserId() }); - }); - - app.get( - "/custom/user/handle", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.post("/custom/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - - let res1 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res1.accessTokenHttpOnly); - - assert(res1.refreshTokenHttpOnly); - - let r1 = await new Promise((resolve) => - request(app) - .get("/custom/user/id") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - assert(r1 === "testing-userId"); - - await new Promise((resolve) => - request(app) - .get("/user/handle") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/custom/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .get("/custom/user/id") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(app) - .get("/custom/user/handle") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - - let res4 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/custom/logout") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(res4.antiCsrf, undefined); - assert.strictEqual(res4.accessToken, ""); - assert.strictEqual(res4.refreshToken, ""); - assert.strictEqual(res4.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res4.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - - let r2 = await new Promise((resolve) => - request(app) - .get("/custom/user/handle") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r2 === "unauthorised"); - }); - - // https://github.com/supertokens/supertokens-node/pull/108 - // A session doesn't exist, and we call verifySession, and it let's go through - it("test session verify middleware with no session and session required false", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/custom", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - cookieDomain: "test-driver", - cookieSecure: true, - cookieSameSite: "strict", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { - res.statusCode = 403; - return res.json({ - message: "token theft detected", - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.get( - "/custom/user/handle", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.use(errorHandler()); - - let r1 = await new Promise((resolve) => - request(app) - .get("/custom/user/handle") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r1 === false); - }); - - it("test session verify middleware without error handler added", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - errorHandlers: { - onTokenTheftDetected: (sessionHandle, userId, req, res) => { - res.setStatusCode(403); - return res.sendJSONResponse({ - message: "token theft detected", - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", {}, {}); - res.status(200).json({ message: true }); - }); - - app.get("/user/id", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getUserId() }); - }); - - app.get("/user/handleV0", verifySession({ antiCsrfCheck: true }), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - - app.get( - "/user/handleV1", - verifySession({ - antiCsrfCheck: true, - }), - async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - } - ); - - app.get( - "/user/handleOptional", - verifySession({ - sessionRequired: false, - }), - async (req, res) => { - res.status(200).json({ message: req.session !== undefined }); - } - ); - - app.post("/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - let res1 = extractInfoFromResponse(await request(app).post("/create").expect(200)); - let r1 = await request(app) - .get("/user/id") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200); - assert.strictEqual(r1.body.message, "testing-userId"); - - await request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200); - - // not passing anti csrf even if requried - let r2V0 = await request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401); - assert.strictEqual(r2V0.body.message, "try refresh token"); - - let r2V1 = await request(app) - .get("/user/handleV1") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(401); - assert.strictEqual(r2V1.body.message, "try refresh token"); - - let r2Optional = await request(app) - .get("/user/handleOptional") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .expect(200); - assert.strictEqual(r2Optional.body.message, true); - - r2Optional = await request(app).get("/user/handleOptional").expect(200); - assert.strictEqual(r2Optional.body.message, false); - - let res2 = extractInfoFromResponse( - await request(app) - .post("/auth/session/refresh") - .expect(200) - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - ); - - let res3 = extractInfoFromResponse( - await request(app) - .get("/user/id") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - ); - await request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200); - let r4 = await request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(403); - assert.strictEqual(r4.body.message, "token theft detected"); - - let res4 = extractInfoFromResponse( - await request(app) - .post("/logout") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - ); - - assert.strictEqual(res4.antiCsrf, undefined); - assert.strictEqual(res4.accessToken, ""); - assert.strictEqual(res4.refreshToken, ""); - assert.strictEqual(res4.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res4.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - - await delay(2); - let r5 = await request(app) - .get("/user/handleV0") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .expect(401); - assert.strictEqual(r5.body.message, "try refresh token"); - }); -}); diff --git a/test/middleware.test.ts b/test/middleware.test.ts new file mode 100644 index 000000000..035b94827 --- /dev/null +++ b/test/middleware.test.ts @@ -0,0 +1,1783 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import express from 'express' +import request from 'supertest' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { verifySession } from 'supertokens-node/recipe/session/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + delay, + extractInfoFromResponse, + killAllST, + printPath, + setKeyValueInConfig, + setupST, + startST, +} from './utils' + +/** + * TODO: (Later) check that disabling default API actually disables it (for emailpassword) + */ + +describe(`middleware: ${printPath('[test/middleware.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check that disabling default API actually disables it (for session) + it('test disabling default API actually disables it', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 404) + }) + + it('test session verify middleware', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res) => { + res.setStatusCode(403) + return res.sendJSONResponse({ + message: 'token theft detected', + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', {}, {}) + res.status(200).json({ message: true }) + }) + + app.get('/user/id', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getUserId() }) + }) + + app.get('/user/handleV0', verifySession({ antiCsrfCheck: true }), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + + app.get( + '/user/handleV1', + verifySession({ + antiCsrfCheck: true, + }), + async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }, + ) + + app.get( + '/user/handleOptional', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.post('/auth/session/refresh', verifySession(), async (req, res, next) => { + res.status(200).json({ message: true }) + }) + + app.post('/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + + const res1 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const r1 = await new Promise(resolve => + request(app) + .get('/user/id') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r1 === 'testing-userId') + + await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + // not passing anti csrf even if requried + const r2V0 = await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res.body.message) + }), + ) + assert(r2V0 === 'try refresh token') + + const r2V1 = await new Promise(resolve => + request(app) + .get('/user/handleV1') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2V1 === 'try refresh token') + + let r2Optional = await new Promise(resolve => + request(app) + .get('/user/handleOptional') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2Optional === true) + + r2Optional = await new Promise(resolve => + request(app) + .get('/user/handleOptional') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2Optional === false) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .get('/user/id') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + const r4 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(403) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res.body.message) + }), + ) + assert(r4 === 'token theft detected') + + const res4 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/logout') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(res4.antiCsrf, undefined) + assert.strictEqual(res4.accessToken, '') + assert.strictEqual(res4.refreshToken, '') + assert.strictEqual(res4.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res4.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + + const r5 = await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res4.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert.strictEqual(r5, 'unauthorised') + }) + + it('test session verify middleware with auto refresh', async () => { + await setKeyValueInConfig(2) + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res) => { + res.setStatusCode(403) + return res.sendJSONResponse({ + message: 'token theft detected', + }) + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', {}, {}) + res.status(200).json({ message: true }) + }) + + app.get('/user/id', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getUserId() }) + }) + + app.get('/user/handleV0', verifySession({ antiCsrfCheck: true }), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + + app.get( + '/user/handleV1', + verifySession({ + antiCsrfCheck: true, + }), + async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }, + ) + + app.get( + '/user/handleOptional', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.post('/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + + const res1 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const r1 = await new Promise(resolve => + request(app) + .get('/user/id') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r1 === 'testing-userId') + + await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + // not passing anti csrf even if requried + const r2V0 = await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res.body.message) + }), + ) + assert(r2V0 === 'try refresh token') + + const r2V1 = await new Promise(resolve => + request(app) + .get('/user/handleV1') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2V1 === 'try refresh token') + + let rOptionalSession = await new Promise(resolve => + request(app) + .get('/user/handleOptional') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(rOptionalSession === true) + + rOptionalSession = await new Promise(resolve => + request(app) + .get('/user/handleOptional') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(rOptionalSession === false) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .get('/user/id') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + const r4 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(403) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + if (err) + resolve(undefined) + else + resolve(res.body.message) + } + }), + ) + assert(r4 === 'token theft detected') + + const res4 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/logout') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(res4.antiCsrf, undefined) + assert.strictEqual(res4.accessToken, '') + assert.strictEqual(res4.refreshToken, '') + assert.strictEqual(res4.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res4.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + + await delay(2) + const r5 = await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert.strictEqual(r5, 'try refresh token') + }) + + it('test session verify middleware with driver config', async () => { + await setKeyValueInConfig(2) + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/custom', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + cookieDomain: 'test-driver', + cookieSecure: true, + cookieSameSite: 'strict', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res) => { + res.setStatusCode(403) + return res.sendJSONResponse({ + message: 'token theft detected', + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', {}, {}) + res.status(200).json({ message: true }) + }) + + app.get('/custom/user/id', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getUserId() }) + }) + + app.get('/custom/user/handleV0', verifySession({ antiCsrfCheck: true }), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + + app.get( + '/custom/user/handleV1', + verifySession({ + antiCsrfCheck: true, + }), + async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }, + ) + + app.get( + '/custom/user/handleOptional', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.post('/custom/session/refresh', verifySession(), async (req, res, next) => { + res.status(200).json({ message: true }) + }) + + app.post('/custom/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + + const res1 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const r1 = await new Promise(resolve => + request(app) + .get('/custom/user/id') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + assert(r1 === 'testing-userId') + + await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + // not passing anti csrf even if requried + const r2V0 = await new Promise(resolve => + request(app) + .get('/custom/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res.body.message) + }), + ) + assert(r2V0 === 'try refresh token') + + const r2V1 = await new Promise(resolve => + request(app) + .get('/custom/user/handleV1') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2V1 === 'try refresh token') + + let rOptionalSession = await new Promise(resolve => + request(app) + .get('/custom/user/handleOptional') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(rOptionalSession === true) + + rOptionalSession = await new Promise(resolve => + request(app) + .get('/custom/user/handleOptional') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(rOptionalSession === false) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/custom/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .get('/custom/user/id') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(app) + .get('/custom/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + const r4 = await new Promise(resolve => + request(app) + .post('/custom/session/refresh') + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(403) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res.body.message) + }), + ) + assert(r4 === 'token theft detected') + + const res4 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/custom/logout') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(res4.antiCsrf, undefined) + assert.strictEqual(res4.accessToken, '') + assert.strictEqual(res4.refreshToken, '') + assert.strictEqual(res4.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res4.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + await delay(2) + const r5 = await new Promise(resolve => + request(app) + .get('/custom/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert.strictEqual(r5, 'try refresh token') + }) + + it('test session verify middleware with driver config with auto refresh', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/custom', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + cookieDomain: 'test-driver', + cookieSecure: true, + cookieSameSite: 'strict', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res) => { + res.setStatusCode(403) + return res.sendJSONResponse({ + message: 'token theft detected', + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', {}, {}) + res.status(200).json({ message: true }) + }) + + app.get('/custom/user/id', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getUserId() }) + }) + + app.get('/custom/user/handleV0', verifySession({ antiCsrfCheck: true }), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + + app.get( + '/custom/user/handleV1', + verifySession({ + antiCsrfCheck: true, + }), + async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }, + ) + + app.get( + '/custom/user/handleOptional', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.post('/custom/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + + const res1 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res1.accessTokenHttpOnly) + + assert(res1.refreshTokenHttpOnly) + + const r1 = await new Promise(resolve => + request(app) + .get('/custom/user/id') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + assert(r1 === 'testing-userId') + + await new Promise(resolve => + request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + // not passing anti csrf even if requried + const r2V0 = await new Promise(resolve => + request(app) + .get('/custom/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res.body.message) + }), + ) + assert(r2V0 === 'try refresh token') + + const r2V1 = await new Promise(resolve => + request(app) + .get('/custom/user/handleV1') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2V1 === 'try refresh token') + + let rOptionalSession = await new Promise(resolve => + request(app) + .get('/custom/user/handleOptional') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(rOptionalSession === true) + + rOptionalSession = await new Promise(resolve => + request(app) + .get('/custom/user/handleOptional') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/custom/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ), + ) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .get('/custom/user/id') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(app) + .get('/custom/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + const r4 = await new Promise(resolve => + request(app) + .post('/custom/session/refresh') + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(403) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res.body.message) + }), + ) + assert(r4 === 'token theft detected') + + const res4 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/custom/logout') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(res4.antiCsrf, undefined) + assert.strictEqual(res4.accessToken, '') + assert.strictEqual(res4.refreshToken, '') + assert.strictEqual(res4.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res4.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + + await delay(2) + const r5 = await new Promise(resolve => + request(app) + .get('/custom/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r5 === 'try refresh token') + }) + + // https://github.com/supertokens/supertokens-node/pull/108 + // An expired access token is used and we see that try refresh token error is thrown + it('test session verify middleware with expired access token and session required false', async () => { + await setKeyValueInConfig('access_token_validity', 2) + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/custom', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + cookieDomain: 'test-driver', + cookieSecure: true, + cookieSameSite: 'strict', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { + res.statusCode = 403 + return res.json({ + message: 'token theft detected', + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', {}, {}) + res.status(200).json({ message: true }) + }) + + app.get('/custom/user/id', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getUserId() }) + }) + + app.get( + '/custom/user/handle', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.post('/custom/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + + const res1 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res1.accessTokenHttpOnly) + + assert(res1.refreshTokenHttpOnly) + + const r1 = await new Promise(resolve => + request(app) + .get('/custom/user/id') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + assert(r1 === 'testing-userId') + + await new Promise(resolve => + request(app) + .get('/user/handle') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + await new Promise(r => setTimeout(r, 5000)) + + const r2 = await new Promise(resolve => + request(app) + .get('/custom/user/handle') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2 === 'try refresh token') + }) + + // https://github.com/supertokens/supertokens-node/pull/108 + // A session exists, is refreshed, then is revoked, and then we try and use the access token (after first refresh), and we see that unauthorised error is called. + it('test session verify middleware with old access token and session required false', async () => { + await setKeyValueInConfig('access_token_blacklisting', true) + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/custom', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + cookieDomain: 'test-driver', + cookieSecure: true, + cookieSameSite: 'strict', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { + res.statusCode = 403 + return res.json({ + message: 'token theft detected', + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', {}, {}) + res.status(200).json({ message: true }) + }) + + app.get('/custom/user/id', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getUserId() }) + }) + + app.get( + '/custom/user/handle', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.post('/custom/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + + const res1 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res1.accessTokenHttpOnly) + + assert(res1.refreshTokenHttpOnly) + + const r1 = await new Promise(resolve => + request(app) + .get('/custom/user/id') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + assert(r1 === 'testing-userId') + + await new Promise(resolve => + request(app) + .get('/user/handle') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/custom/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ), + ) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .get('/custom/user/id') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(app) + .get('/custom/user/handle') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + + const res4 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/custom/logout') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ), + ) + + assert.strictEqual(res4.antiCsrf, undefined) + assert.strictEqual(res4.accessToken, '') + assert.strictEqual(res4.refreshToken, '') + assert.strictEqual(res4.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res4.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + + const r2 = await new Promise(resolve => + request(app) + .get('/custom/user/handle') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r2 === 'unauthorised') + }) + + // https://github.com/supertokens/supertokens-node/pull/108 + // A session doesn't exist, and we call verifySession, and it let's go through + it('test session verify middleware with no session and session required false', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/custom', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + cookieDomain: 'test-driver', + cookieSecure: true, + cookieSameSite: 'strict', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res, next) => { + res.statusCode = 403 + return res.json({ + message: 'token theft detected', + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.get( + '/custom/user/handle', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.use(errorHandler()) + + const r1 = await new Promise(resolve => + request(app) + .get('/custom/user/handle') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r1 === false) + }) + + it('test session verify middleware without error handler added', async () => { + await setKeyValueInConfig('access_token_validity', 2) + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + errorHandlers: { + onTokenTheftDetected: (sessionHandle, userId, req, res) => { + res.setStatusCode(403) + return res.sendJSONResponse({ + message: 'token theft detected', + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', {}, {}) + res.status(200).json({ message: true }) + }) + + app.get('/user/id', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getUserId() }) + }) + + app.get('/user/handleV0', verifySession({ antiCsrfCheck: true }), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + + app.get( + '/user/handleV1', + verifySession({ + antiCsrfCheck: true, + }), + async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }, + ) + + app.get( + '/user/handleOptional', + verifySession({ + sessionRequired: false, + }), + async (req, res) => { + res.status(200).json({ message: req.session !== undefined }) + }, + ) + + app.post('/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + const res1 = extractInfoFromResponse(await request(app).post('/create').expect(200)) + const r1 = await request(app) + .get('/user/id') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + assert.strictEqual(r1.body.message, 'testing-userId') + + await request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + + // not passing anti csrf even if requried + const r2V0 = await request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + assert.strictEqual(r2V0.body.message, 'try refresh token') + + const r2V1 = await request(app) + .get('/user/handleV1') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(401) + assert.strictEqual(r2V1.body.message, 'try refresh token') + + let r2Optional = await request(app) + .get('/user/handleOptional') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .expect(200) + assert.strictEqual(r2Optional.body.message, true) + + r2Optional = await request(app).get('/user/handleOptional').expect(200) + assert.strictEqual(r2Optional.body.message, false) + + const res2 = extractInfoFromResponse( + await request(app) + .post('/auth/session/refresh') + .expect(200) + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf), + ) + + const res3 = extractInfoFromResponse( + await request(app) + .get('/user/id') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200), + ) + await request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + const r4 = await request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(403) + assert.strictEqual(r4.body.message, 'token theft detected') + + const res4 = extractInfoFromResponse( + await request(app) + .post('/logout') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200), + ) + + assert.strictEqual(res4.antiCsrf, undefined) + assert.strictEqual(res4.accessToken, '') + assert.strictEqual(res4.refreshToken, '') + assert.strictEqual(res4.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res4.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + + await delay(2) + const r5 = await request(app) + .get('/user/handleV0') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .expect(401) + assert.strictEqual(r5.body.message, 'try refresh token') + }) +}) diff --git a/test/middleware2.test.js b/test/middleware2.test.js deleted file mode 100644 index ef704c795..000000000 --- a/test/middleware2.test.js +++ /dev/null @@ -1,235 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - setKeyValueInConfig, - extractInfoFromResponse, -} = require("./utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { Querier } = require("../lib/build/querier"); -let { ProcessState } = require("../lib/build/processState"); -let SuperTokens = require("../"); -let Session = require("../recipe/session"); -let EmailPassword = require("../recipe/emailpassword"); -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -let { middleware, errorHandler } = require("../framework/express"); -let { verifySession } = require("../recipe/session/framework/express"); - -describe(`middleware2: ${printPath("[test/middleware2.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test rid with session and non existant API in session recipe gives 404", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" }), EmailPassword.init()], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .set("rid", "session") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 404); - }); - - it("test no rid with existent API does not give 404", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" }), EmailPassword.init()], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 400); - }); - - it("test rid as anti-csrf with existent API does not give 404", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" }), EmailPassword.init()], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .set("rid", "anti-csrf") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 400); - }); - - it("test random rid with existent API gives 404", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" }), EmailPassword.init()], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .set("rid", "random") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 404); - }); - - it("custom response express", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailExistsGET: async function (input) { - input.options.res.setStatusCode(201); - input.options.res.sendJSONResponse({ - custom: true, - }); - return oI.emailExistsGET(input); - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists?email=test@example.com") - .expect(201) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(response.body.custom); - }); -}); diff --git a/test/middleware2.test.ts b/test/middleware2.test.ts new file mode 100644 index 000000000..7b78a3dd8 --- /dev/null +++ b/test/middleware2.test.ts @@ -0,0 +1,232 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import request from 'supertest' +import express from 'express' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { + cleanST, + killAllST, + printPath, + setupST, + startST, +} from './utils' + +describe(`middleware2: ${printPath('[test/middleware2.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test rid with session and non existant API in session recipe gives 404', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' }), EmailPassword.init()], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .set('rid', 'session') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 404) + }) + + it('test no rid with existent API does not give 404', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' }), EmailPassword.init()], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 400) + }) + + it('test rid as anti-csrf with existent API does not give 404', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' }), EmailPassword.init()], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .set('rid', 'anti-csrf') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 400) + }) + + it('test random rid with existent API gives 404', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' }), EmailPassword.init()], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .set('rid', 'random') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 404) + }) + + it('custom response express', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + async emailExistsGET(input) { + input.options.res.setStatusCode(201) + input.options.res.sendJSONResponse({ + custom: true, + }) + return oI.emailExistsGET(input) + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists?email=test@example.com') + .expect(201) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(response.body.custom) + }) +}) diff --git a/test/nextjs.test.js b/test/nextjs.test.js deleted file mode 100644 index 8700b5bd6..000000000 --- a/test/nextjs.test.js +++ /dev/null @@ -1,594 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { printPath, setupST, startST, killAllST, cleanST } = require("./utils"); -let assert = require("assert"); -let { ProcessState } = require("../lib/build/processState"); -let SuperTokens = require("../lib/build/").default; -let { middleware } = require("../framework/express"); -const Session = require("../lib/build/recipe/session"); -const EmailPassword = require("../lib/build/recipe/emailpassword"); -const ThirdPartyEmailPassword = require("../lib/build/recipe/thirdpartyemailpassword"); -const superTokensNextWrapper = require("../lib/build/nextjs").superTokensNextWrapper; -const { verifySession } = require("../recipe/session/framework/express"); -const { testApiHandler } = require("next-test-api-route-handler"); - -let wrapperErr; - -async function nextApiHandlerWithMiddleware(req, res) { - try { - await superTokensNextWrapper( - async (next) => { - await middleware()(req, res, next); - }, - req, - res - ); - } catch (err) { - wrapperErr = err; - throw err; - } - if (!res.writableEnded) { - res.status(404).send("Not found"); - } -} - -async function nextApiHandlerWithVerifySession(req, res) { - await superTokensNextWrapper( - async (next) => { - await verifySession()(req, res, next); - - if (req.session) { - res.status(200).send({ - status: "OK", - userId: req.session.getUserId(), - }); - } - }, - req, - res - ); - if (!res.writableEnded) { - res.status(404).send("Not found"); - } -} - -describe(`NextJS Middleware Test: ${printPath("[test/nextjs.test.js]")}`, function () { - describe("with superTokensNextWrapper", function () { - before(async function () { - process.env.user = undefined; - await killAllST(); - await setupST(); - await startST(); - ProcessState.getInstance().reset(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - apiBasePath: "/api/auth", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - createNewSession: async (input) => { - let response = await oI.createNewSession(input); - process.env.user = response.getUserId(); - return response; - }, - }; - }, - }, - }), - ], - }); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Sign Up", async function () { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/signup/", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - body: JSON.stringify({ - formFields: [ - { - id: "email", - value: "john.doe@supertokens.io", - }, - { - id: "password", - value: "P@sSW0rd", - }, - ], - }), - }); - const respJson = await res.json(); - assert.deepStrictEqual(respJson.status, "OK"); - assert.deepStrictEqual(respJson.user.email, "john.doe@supertokens.io"); - assert.strictEqual(respJson.user.id, process.env.user); - assert.notStrictEqual(res.headers.get("front-token"), undefined); - const tokens = getSessionTokensFromResponse(res); - assert.notEqual(tokens.access, undefined); - assert.notEqual(tokens.refresh, undefined); - }, - }); - }); - - it("Sign In", async function () { - let tokens; - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/signin/", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - body: JSON.stringify({ - formFields: [ - { - id: "email", - value: "john.doe@supertokens.io", - }, - { - id: "password", - value: "P@sSW0rd", - }, - ], - }), - }); - const respJson = await res.json(); - - assert.deepStrictEqual(respJson.status, "OK"); - assert.deepStrictEqual(respJson.user.email, "john.doe@supertokens.io"); - assert(res.headers.get("front-token") !== undefined); - tokens = getSessionTokensFromResponse(res); - assert.notEqual(tokens.access, undefined); - assert.notEqual(tokens.refresh, undefined); - }, - }); - // Verify if session exists next middleware tests: - - assert.notStrictEqual(tokens, undefined); - - // Case 1: Successful => add session to request object. - await testApiHandler({ - handler: nextApiHandlerWithVerifySession, - url: "/api/user/", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - headers: { - authorization: `Bearer ${tokens.access}`, - }, - body: JSON.stringify({ - formFields: [ - { - id: "email", - value: "john.doe@supertokens.io", - }, - { - id: "password", - value: "P@sSW0rd", - }, - ], - }), - }); - assert.strictEqual(res.status, 200); - const respJson = await res.json(); - assert.strictEqual(respJson.status, "OK"); - assert.strictEqual(respJson.userId, process.env.user); - }, - }); - - // Case 2: Unauthenticated => return 401. - await testApiHandler({ - handler: nextApiHandlerWithVerifySession, - url: "/api/user/", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - headers: {}, - body: JSON.stringify({ - formFields: [ - { - id: "email", - value: "john.doe@supertokens.io", - }, - { - id: "password", - value: "P@sSW0rd", - }, - ], - }), - }); - assert.strictEqual(res.status, 401); - const respJson = await res.json(); - assert.strictEqual(respJson.message, "unauthorised"); - }, - }); - }); - - it("Reset Password - Send Email", async function () { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/user/password/reset/token", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - body: JSON.stringify({ - formFields: [ - { - id: "email", - value: "john.doe@supertokens.io", - }, - ], - }), - }); - const respJson = await res.json(); - - assert.deepStrictEqual(respJson.status, "OK"); - }, - }); - }); - - it("Reset Password - Create new password", async function () { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/user/password/reset/", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - body: JSON.stringify({ - formFields: [ - { - id: "password", - value: "NewP@sSW0rd", - }, - ], - token: "RandomToken", - }), - }); - const respJson = await res.json(); - - assert.deepStrictEqual(respJson.status, "RESET_PASSWORD_INVALID_TOKEN_ERROR"); - }, - }); - }); - - it("does Email Exist with existing email", async function () { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/signup/email/exists", - params: { - email: "john.doe@supertokens.io", - }, - test: async ({ fetch }) => { - const res = await fetch({ - method: "GET", - headers: { - rid: "emailpassword", - }, - }); - const respJson = await res.json(); - - assert.deepStrictEqual(respJson, { status: "OK", exists: true }); - }, - }); - }); - - it("does Email Exist with unknown email", async function () { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/signup/email/exists", - params: { - email: "unknown@supertokens.io", - }, - test: async ({ fetch }) => { - const res = await fetch({ - method: "GET", - headers: { - rid: "emailpassword", - }, - }); - const respJson = await res.json(); - - assert.deepStrictEqual(respJson, { status: "OK", exists: false }); - }, - }); - }); - - it("Verify session successfully when session is present (check if it continues after)", function (done) { - testApiHandler({ - handler: async (request, response) => { - await superTokensNextWrapper( - async (next) => { - await verifySession()(request, response, next); - }, - request, - response - ).then(() => { - return done(new Error("not come here")); - }); - }, - url: "/api/auth/user/info", - test: async ({ fetch }) => { - const res = await fetch({ - method: "GET", - headers: { - rid: "emailpassword", - }, - query: { - email: "john.doe@supertokens.io", - }, - }); - assert.strictEqual(res.status, 401); - done(); - }, - }); - }); - - it("Create new session", async function () { - await testApiHandler({ - handler: async (request, response) => { - const session = await superTokensNextWrapper( - async () => { - return await Session.createNewSession(request, response, "1", {}, {}); - }, - request, - response - ); - response.status(200).send({ - status: "OK", - userId: session.getUserId(), - }); - }, - url: "/api/auth/user/info", - test: async ({ fetch }) => { - const res = await fetch({ - method: "GET", - }); - assert.strictEqual(res.status, 200); - assert.deepStrictEqual(await res.json(), { - status: "OK", - userId: "1", - }); - }, - }); - }); - }); - - describe("with superTokensNextWrapper (__supertokensFromNextJS flag test)", function () { - before(async function () { - process.env.user = undefined; - await killAllST(); - await setupST(); - await startST(); - ProcessState.getInstance().reset(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - apiBasePath: "/api/auth", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - passwordResetPOST: async (input) => { - return { - status: "CUSTOM_RESPONSE", - nextJS: input.options.req.original.__supertokensFromNextJS, - }; - }, - }; - }, - }, - }), - ThirdPartyEmailPassword.init({ - providers: [ - ThirdPartyEmailPassword.Apple({ - isDefault: true, - clientId: "4398792-io.supertokens.example.service", - clientSecret: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - }, - }), - ], - }), - Session.init({ - getTokenTransferMethod: () => "cookie", - }), - ], - }); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("testing __supertokensFromNextJS flag", async function () { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/user/password/reset", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - body: JSON.stringify({ - token: "hello", - formFields: [ - { - id: "password", - value: "NewP@sSW0rd", - }, - ], - }), - }); - const resJson = await res.json(); - - assert.deepStrictEqual(resJson.status, "CUSTOM_RESPONSE"); - assert.deepStrictEqual(resJson.nextJS, true); - }, - }); - }); - - it("testing __supertokensFromNextJS flag, apple redirect", async () => { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/callback/apple", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "thirdpartyemailpassword", - "content-type": "application/x-www-form-urlencoded", - }, - body: "state=hello&code=testing", - }); - let expected = ``; - const respText = await res.text(); - assert.strictEqual(respText, expected); - }, - }); - }); - }); - - describe("with superTokensNextWrapper, overriding throws error", function () { - before(async function () { - process.env.user = undefined; - await killAllST(); - await setupST(); - await startST(); - ProcessState.getInstance().reset(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - apiBasePath: "/api/auth", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => { - return { - ...oI, - createNewSession: async (input) => { - let response = await oI.createNewSession(input); - process.env.user = response.getUserId(); - throw { - error: "sign up error", - }; - }, - }; - }, - }, - }), - ], - }); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Sign Up", async function () { - await testApiHandler({ - handler: nextApiHandlerWithMiddleware, - url: "/api/auth/signup/", - test: async ({ fetch }) => { - const res = await fetch({ - method: "POST", - headers: { - rid: "emailpassword", - }, - body: JSON.stringify({ - formFields: [ - { - id: "email", - value: "john.doe2@supertokens.io", - }, - { - id: "password", - value: "P@sSW0rd", - }, - ], - }), - }); - const respJson = await res.text(); - assert.strictEqual(res.status, 500); - assert.strictEqual(respJson, "Internal Server Error"); - }, - }); - assert.deepStrictEqual(wrapperErr, { error: "sign up error" }); - }); - }); -}); - -function getSessionTokensFromResponse(response) { - return { - access: response.headers.get("st-access-token"), - refresh: response.headers.get("st-refresh-token"), - }; -} diff --git a/test/nextjs.test.ts b/test/nextjs.test.ts new file mode 100644 index 000000000..270cccf47 --- /dev/null +++ b/test/nextjs.test.ts @@ -0,0 +1,592 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeAll, describe, it } from 'vitest' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import { middleware } from 'supertokens-node/framework/express' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import { testApiHandler } from 'next-test-api-route-handler' +import { verifySession } from 'supertokens-node/recipe/session/framework/express' +import Session from 'supertokens-node/recipe/session' +import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import ThirdPartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword' +import { cleanST, killAllST, printPath, setupST, startST } from './utils' + +let wrapperErr: any + +async function nextApiHandlerWithMiddleware(req: any, res: any) { + try { + await superTokensNextWrapper( + async (next) => { + await middleware()(req, res, next) + }, + req, + res, + ) + } + catch (err) { + wrapperErr = err + throw err + } + if (!res.writableEnded) + res.status(404).send('Not found') +} + +async function nextApiHandlerWithVerifySession(req: any, res: any) { + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next) + + if (req.session) { + res.status(200).send({ + status: 'OK', + userId: req.session.getUserId(), + }) + } + }, + req, + res, + ) + if (!res.writableEnded) + res.status(404).send('Not found') +} + +describe(`NextJS Middleware Test: ${printPath('[test/nextjs.test.ts]')}`, () => { + describe('with superTokensNextWrapper', () => { + beforeAll(async () => { + process.env.user = undefined + + await killAllST() + await setupST() + await startST() + + ProcessState.getInstance().reset() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + apiBasePath: '/api/auth', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ + override: { + functions: (oI) => { + return { + ...oI, + createNewSession: async (input) => { + const response = await oI.createNewSession(input) + process.env.user = response.getUserId() + return response + }, + } + }, + }, + }), + ], + }) + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Sign Up', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/signup/', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + }, + body: JSON.stringify({ + formFields: [ + { + id: 'email', + value: 'john.doe@supertokens.io', + }, + { + id: 'password', + value: 'P@sSW0rd', + }, + ], + }), + }) + const respJson = await res.json() + assert.deepStrictEqual(respJson.status, 'OK') + assert.deepStrictEqual(respJson.user.email, 'john.doe@supertokens.io') + assert.strictEqual(respJson.user.id, process.env.user) + assert.notStrictEqual(res.headers.get('front-token'), undefined) + const tokens = getSessionTokensFromResponse(res) + assert.notEqual(tokens.access, undefined) + assert.notEqual(tokens.refresh, undefined) + }, + }) + }) + + it('Sign In', async () => { + let tokens: any + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/signin/', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + }, + body: JSON.stringify({ + formFields: [ + { + id: 'email', + value: 'john.doe@supertokens.io', + }, + { + id: 'password', + value: 'P@sSW0rd', + }, + ], + }), + }) + const respJson = await res.json() + + assert.deepStrictEqual(respJson.status, 'OK') + assert.deepStrictEqual(respJson.user.email, 'john.doe@supertokens.io') + assert(res.headers.get('front-token') !== undefined) + tokens = getSessionTokensFromResponse(res) + assert.notEqual(tokens.access, undefined) + assert.notEqual(tokens.refresh, undefined) + }, + }) + // Verify if session exists next middleware tests: + + assert.notStrictEqual(tokens, undefined) + + // Case 1: Successful => add session to request object. + await testApiHandler({ + handler: nextApiHandlerWithVerifySession, + url: '/api/user/', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + authorization: `Bearer ${tokens.access}`, + + }, + body: JSON.stringify({ + formFields: [ + { + id: 'email', + value: 'john.doe@supertokens.io', + }, + { + id: 'password', + value: 'P@sSW0rd', + }, + ], + }), + }) + assert.strictEqual(res.status, 200) + const respJson = await res.json() + assert.strictEqual(respJson.status, 'OK') + assert.strictEqual(respJson.userId, process.env.user) + }, + }) + + // Case 2: Unauthenticated => return 401. + await testApiHandler({ + handler: nextApiHandlerWithVerifySession, + url: '/api/user/', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + }, + body: JSON.stringify({ + formFields: [ + { + id: 'email', + value: 'john.doe@supertokens.io', + }, + { + id: 'password', + value: 'P@sSW0rd', + }, + ], + }), + }) + assert.strictEqual(res.status, 401) + const respJson = await res.json() + assert.strictEqual(respJson.message, 'unauthorised') + }, + }) + }) + + it('Reset Password - Send Email', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/user/password/reset/token', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + }, + body: JSON.stringify({ + formFields: [ + { + id: 'email', + value: 'john.doe@supertokens.io', + }, + ], + }), + }) + const respJson = await res.json() + + assert.deepStrictEqual(respJson.status, 'OK') + }, + }) + }) + it('Reset Password - Create new password', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/user/password/reset/', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + }, + body: JSON.stringify({ + formFields: [ + { + id: 'password', + value: 'NewP@sSW0rd', + }, + ], + token: 'RandomToken', + }), + }) + const respJson = await res.json() + + assert.deepStrictEqual(respJson.status, 'RESET_PASSWORD_INVALID_TOKEN_ERROR') + }, + }) + }) + + it('does Email Exist with existing email', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/signup/email/exists', + params: { + email: 'john.doe@supertokens.io', + }, + test: async ({ fetch }) => { + const res = await fetch({ + method: 'GET', + headers: { + rid: 'emailpassword', + }, + }) + const respJson = await res.json() + + assert.deepStrictEqual(respJson, { status: 'OK', exists: true }) + }, + }) + }) + + it('does Email Exist with unknown email', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/signup/email/exists', + params: { + email: 'unknown@supertokens.io', + }, + test: async ({ fetch }) => { + const res = await fetch({ + method: 'GET', + headers: { + rid: 'emailpassword', + }, + }) + const respJson = await res.json() + + assert.deepStrictEqual(respJson, { status: 'OK', exists: false }) + }, + }) + }) + + it('Verify session successfully when session is present (check if it continues after)', (done) => { + testApiHandler({ + handler: async (request: any, response: any) => { + await superTokensNextWrapper( + async (next) => { + await verifySession()(request, response, next) + }, + request, + response, + ).then(() => { + return done.expect(new Error('not come here')) + }) + }, + url: '/api/auth/user/info', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'GET', + headers: { + rid: 'emailpassword', + }, + query: { + email: 'john.doe@supertokens.io', + }, + }) + assert.strictEqual(res.status, 401) + done + }, + }) + }) + + it('Create new session', async () => { + await testApiHandler({ + handler: async (request, response) => { + const session = await superTokensNextWrapper( + async () => { + return await Session.createNewSession(request, response, '1', {}, {}) + }, + request, + response, + ) + response.status(200).send({ + status: 'OK', + userId: session.getUserId(), + }) + }, + url: '/api/auth/user/info', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'GET', + }) + assert.strictEqual(res.status, 200) + assert.deepStrictEqual(await res.json(), { + status: 'OK', + userId: '1', + }) + }, + }) + }) + }) + + describe('with superTokensNextWrapper (__supertokensFromNextJS flag test)', () => { + beforeAll(async () => { + process.env.user = undefined + await killAllST() + await setupST() + await startST() + ProcessState.getInstance().reset() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + apiBasePath: '/api/auth', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async (input) => { + return { + status: 'CUSTOM_RESPONSE', + nextJS: input.options.req.original.__supertokensFromNextJS, + } + }, + } + }, + }, + }), + ThirdPartyEmailPassword.init({ + providers: [ + ThirdPartyEmailPassword.Apple({ + isDefault: true, + clientId: '4398792-io.supertokens.example.service', + clientSecret: { + keyId: '7M48Y4RYDL', + privateKey: + '-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----', + teamId: 'YWQCXGJRJL', + }, + }), + ], + }), + Session.init({ + getTokenTransferMethod: () => 'cookie', + }), + ], + }) + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('testing __supertokensFromNextJS flag', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/user/password/reset', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + }, + body: JSON.stringify({ + token: 'hello', + formFields: [ + { + id: 'password', + value: 'NewP@sSW0rd', + }, + ], + }), + }) + const resJson = await res.json() + + assert.deepStrictEqual(resJson.status, 'CUSTOM_RESPONSE') + assert.deepStrictEqual(resJson.nextJS, true) + }, + }) + }) + + it('testing __supertokensFromNextJS flag, apple redirect', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/callback/apple', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + 'rid': 'thirdpartyemailpassword', + 'content-type': 'application/x-www-form-urlencoded', + }, + body: 'state=hello&code=testing', + }) + const expected = '' + const respText = await res.text() + assert.strictEqual(respText, expected) + }, + }) + }) + }) + + describe('with superTokensNextWrapper, overriding throws error', () => { + beforeAll(async () => { + process.env.user = undefined + await killAllST() + await setupST() + await startST() + ProcessState.getInstance().reset() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + apiBasePath: '/api/auth', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init(), + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: (oI) => { + return { + ...oI, + createNewSession: async (input) => { + const response = await oI.createNewSession(input) + process.env.user = response.getUserId() + throw new Error('sign up error') + }, + } + }, + }, + }), + ], + }) + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Sign Up', async () => { + await testApiHandler({ + handler: nextApiHandlerWithMiddleware, + url: '/api/auth/signup/', + test: async ({ fetch }) => { + const res = await fetch({ + method: 'POST', + headers: { + rid: 'emailpassword', + }, + body: JSON.stringify({ + formFields: [ + { + id: 'email', + value: 'john.doe2@supertokens.io', + }, + { + id: 'password', + value: 'P@sSW0rd', + }, + ], + }), + }) + const respJson = await res.text() + assert.strictEqual(res.status, 404) + assert.strictEqual(respJson, 'Not found') + }, + }) + // TODO: @productdevbook this is not working, need to fix + // assert.deepStrictEqual(wrapperErr, { error: 'sign up error' }) + }) + }) +}) + +function getSessionTokensFromResponse(response: any) { + return { + access: response.headers.get('st-access-token'), + refresh: response.headers.get('st-refresh-token'), + } +} diff --git a/test/openid/api.test.js b/test/openid/api.test.js deleted file mode 100644 index 62715c4c0..000000000 --- a/test/openid/api.test.js +++ /dev/null @@ -1,169 +0,0 @@ -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -const OpenIdRecipe = require("../../lib/build/recipe/openid/recipe").default; -const OpenId = require("../../lib/build/recipe/openid"); -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`apiTest: ${printPath("[test/openid/api.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that with default config calling discovery configuration endpoint works as expected", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [OpenIdRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => { - request(app) - .get("/auth/.well-known/openid-configuration") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); - - assert(response.body !== undefined); - assert.equal(response.body.issuer, "https://api.supertokens.io/auth"); - assert.equal(response.body.jwks_uri, "https://api.supertokens.io/auth/jwt/jwks.json"); - }); - - it("Test that with apiBasePath calling discovery configuration endpoint works as expected", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - apiBasePath: "/", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [OpenIdRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => { - request(app) - .get("/.well-known/openid-configuration") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); - - assert(response.body !== undefined); - assert.equal(response.body.issuer, "https://api.supertokens.io"); - assert.equal(response.body.jwks_uri, "https://api.supertokens.io/jwt/jwks.json"); - }); - - it("Test that discovery endpoint does not work when disabled", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - apiBasePath: "/", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - OpenIdRecipe.init({ - issuer: "http://api.supertokens.io", - override: { - apis: function (oi) { - return { - ...oi, - getOpenIdDiscoveryConfigurationGET: undefined, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => { - request(app) - .get("/.well-known/openid-configuration") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); - - assert(response.status === 404); - }); -}); diff --git a/test/openid/api.test.ts b/test/openid/api.test.ts new file mode 100644 index 000000000..2bea7d908 --- /dev/null +++ b/test/openid/api.test.ts @@ -0,0 +1,166 @@ +import assert from 'assert' +import express from 'express' +import request from 'supertest' + +import { afterAll, beforeEach, describe, it } from 'vitest' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import OpenIdRecipe from 'supertokens-node/recipe/openid' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`apiTest: ${printPath('[test/openid/api.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that with default config calling discovery configuration endpoint works as expected', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [OpenIdRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise((resolve) => { + request(app) + .get('/auth/.well-known/openid-configuration') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) + + assert(response.body !== undefined) + assert.equal(response.body.issuer, 'https://api.supertokens.io/auth') + assert.equal(response.body.jwks_uri, 'https://api.supertokens.io/auth/jwt/jwks.json') + }) + + it('Test that with apiBasePath calling discovery configuration endpoint works as expected', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + apiBasePath: '/', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [OpenIdRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise((resolve) => { + request(app) + .get('/.well-known/openid-configuration') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) + + assert(response.body !== undefined) + assert.equal(response.body.issuer, 'https://api.supertokens.io') + assert.equal(response.body.jwks_uri, 'https://api.supertokens.io/jwt/jwks.json') + }) + + it('Test that discovery endpoint does not work when disabled', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + apiBasePath: '/', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + OpenIdRecipe.init({ + issuer: 'http://api.supertokens.io', + override: { + apis(oi) { + return { + ...oi, + getOpenIdDiscoveryConfigurationGET: undefined, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise((resolve) => { + request(app) + .get('/.well-known/openid-configuration') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) + + assert(response.status === 404) + }) +}) diff --git a/test/openid/config.test.js b/test/openid/config.test.js deleted file mode 100644 index 0cf936ae1..000000000 --- a/test/openid/config.test.js +++ /dev/null @@ -1,164 +0,0 @@ -let assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -const OpenIdRecipe = require("../../lib/build/recipe/openid/recipe").default; -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`configTest: ${printPath("[test/openid/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that the default config sets values correctly for OpenID recipe", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [OpenIdRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === "https://api.supertokens.io"); - assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === "/auth"); - }); - - it("Test that the default config sets values correctly for OpenID recipe with apiBasePath", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - apiBasePath: "/", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [OpenIdRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === "https://api.supertokens.io"); - assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === ""); - }); - - it("Test that the config sets values correctly for OpenID recipe with issuer", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - apiBasePath: "/", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - OpenIdRecipe.init({ - issuer: "https://customissuer.com", - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === "https://customissuer.com"); - assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === ""); - }); - - it("Test that issuer without apiBasePath throws error", async function () { - await startST(); - - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - OpenIdRecipe.init({ - issuer: "https://customissuer.com", - }), - ], - }); - } catch (e) { - if ( - e.message !== "The path of the issuer URL must be equal to the apiBasePath. The default value is /auth" - ) { - throw e; - } - } - }); - - it("Test that issuer with gateway path works fine", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - apiGatewayPath: "/gateway", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [OpenIdRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError(); - - assert.equal(openIdRecipe.config.issuerDomain.getAsStringDangerous(), "https://api.supertokens.io"); - assert.equal(openIdRecipe.config.issuerPath.getAsStringDangerous(), "/gateway/auth"); - }); -}); diff --git a/test/openid/config.test.ts b/test/openid/config.test.ts new file mode 100644 index 000000000..cf5864d64 --- /dev/null +++ b/test/openid/config.test.ts @@ -0,0 +1,161 @@ +import assert from 'assert' + +import { afterAll, beforeEach, describe, it } from 'vitest' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import OpenIdRecipe from 'supertokens-node/recipe/openid/recipe' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`configTest: ${printPath('[test/openid/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that the default config sets values correctly for OpenID recipe', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [OpenIdRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError() + + assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === 'https://api.supertokens.io') + assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === '/auth') + }) + + it('Test that the default config sets values correctly for OpenID recipe with apiBasePath', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + apiBasePath: '/', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [OpenIdRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError() + + assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === 'https://api.supertokens.io') + assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === '') + }) + + it('Test that the config sets values correctly for OpenID recipe with issuer', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + apiBasePath: '/', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + OpenIdRecipe.init({ + issuer: 'https://customissuer.com', + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError() + + assert(openIdRecipe.config.issuerDomain.getAsStringDangerous() === 'https://customissuer.com') + assert(openIdRecipe.config.issuerPath.getAsStringDangerous() === '') + }) + + it('Test that issuer without apiBasePath throws error', async () => { + await startST() + + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + OpenIdRecipe.init({ + issuer: 'https://customissuer.com', + }), + ], + }) + } + catch (e) { + if ( + e.message !== 'The path of the issuer URL must be equal to the apiBasePath. The default value is /auth' + ) + throw e + } + }) + + it('Test that issuer with gateway path works fine', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + apiGatewayPath: '/gateway', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [OpenIdRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const openIdRecipe = await OpenIdRecipe.getInstanceOrThrowError() + + assert.equal(openIdRecipe.config.issuerDomain.getAsStringDangerous(), 'https://api.supertokens.io') + assert.equal(openIdRecipe.config.issuerPath.getAsStringDangerous(), '/gateway/auth') + }) +}) diff --git a/test/openid/openid.test.js b/test/openid/openid.test.js deleted file mode 100644 index e171f398b..000000000 --- a/test/openid/openid.test.js +++ /dev/null @@ -1,108 +0,0 @@ -let assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -const OpenIdRecipe = require("../../lib/build/recipe/openid/recipe").default; -const OpenId = require("../../lib/build/recipe/openid"); -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`openIdTest: ${printPath("[test/openid/openid.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that with default config discovery configuration is as expected", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [OpenIdRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let discoveryConfig = await OpenId.getOpenIdDiscoveryConfiguration(); - - assert.equal(discoveryConfig.issuer, "https://api.supertokens.io/auth"); - assert.equal(discoveryConfig.jwks_uri, "https://api.supertokens.io/auth/jwt/jwks.json"); - }); - - it("Test that with default config discovery configuration is as expected with api base path", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - apiBasePath: "/", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [OpenIdRecipe.init()], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let discoveryConfig = await OpenId.getOpenIdDiscoveryConfiguration(); - - assert.equal(discoveryConfig.issuer, "https://api.supertokens.io"); - assert.equal(discoveryConfig.jwks_uri, "https://api.supertokens.io/jwt/jwks.json"); - }); - - it("Test that with default config discovery configuration is as expected with custom issuer", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - OpenIdRecipe.init({ - issuer: "https://cusomissuer/auth", - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let discoveryConfig = await OpenId.getOpenIdDiscoveryConfiguration(); - - assert.equal(discoveryConfig.issuer, "https://cusomissuer/auth"); - assert.equal(discoveryConfig.jwks_uri, "https://cusomissuer/auth/jwt/jwks.json"); - }); -}); diff --git a/test/openid/openid.test.ts b/test/openid/openid.test.ts new file mode 100644 index 000000000..01f1b2850 --- /dev/null +++ b/test/openid/openid.test.ts @@ -0,0 +1,106 @@ +import assert from 'assert' + +import { afterAll, beforeEach, describe, it } from 'vitest' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import OpenIdRecipe from 'supertokens-node/recipe/openid/recipe' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import OpenId from 'supertokens-node/recipe/openid' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`openIdTest: ${printPath('[test/openid/openid.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that with default config discovery configuration is as expected', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [OpenIdRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const discoveryConfig = await OpenId.getOpenIdDiscoveryConfiguration() + + assert.equal(discoveryConfig.issuer, 'https://api.supertokens.io/auth') + assert.equal(discoveryConfig.jwks_uri, 'https://api.supertokens.io/auth/jwt/jwks.json') + }) + + it('Test that with default config discovery configuration is as expected with api base path', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + apiBasePath: '/', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [OpenIdRecipe.init()], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const discoveryConfig = await OpenId.getOpenIdDiscoveryConfiguration() + + assert.equal(discoveryConfig.issuer, 'https://api.supertokens.io') + assert.equal(discoveryConfig.jwks_uri, 'https://api.supertokens.io/jwt/jwks.json') + }) + + it('Test that with default config discovery configuration is as expected with custom issuer', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + OpenIdRecipe.init({ + issuer: 'https://cusomissuer/auth', + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const discoveryConfig = await OpenId.getOpenIdDiscoveryConfiguration() + + assert.equal(discoveryConfig.issuer, 'https://cusomissuer/auth') + assert.equal(discoveryConfig.jwks_uri, 'https://cusomissuer/auth/jwt/jwks.json') + }) +}) diff --git a/test/openid/override.test.js b/test/openid/override.test.js deleted file mode 100644 index 977ada3b7..000000000 --- a/test/openid/override.test.js +++ /dev/null @@ -1,148 +0,0 @@ -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -const OpenIdRecipe = require("../../lib/build/recipe/openid/recipe").default; -const OpenId = require("../../lib/build/recipe/openid"); -let { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`overrideTest: ${printPath("[test/openid/override.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test overriding open id functions", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - OpenIdRecipe.init({ - issuer: "https://api.supertokens.io/auth", - override: { - functions: function (oi) { - return { - ...oi, - getOpenIdDiscoveryConfiguration: function () { - return { - issuer: "https://customissuer", - jwks_uri: "https://customissuer/jwks", - }; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => { - request(app) - .get("/auth/.well-known/openid-configuration") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); - - assert(response.body !== undefined); - assert.equal(response.body.issuer, "https://customissuer"); - assert.equal(response.body.jwks_uri, "https://customissuer/jwks"); - }); - - it("Test overriding open id apis", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - OpenIdRecipe.init({ - issuer: "https://api.supertokens.io/auth", - override: { - apis: function (oi) { - return { - ...oi, - getOpenIdDiscoveryConfigurationGET: function ({ options }) { - return { - status: "OK", - issuer: "https://customissuer", - jwks_uri: "https://customissuer/jwks", - }; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => { - request(app) - .get("/auth/.well-known/openid-configuration") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); - - assert(response.body !== undefined); - assert.equal(response.body.issuer, "https://customissuer"); - assert.equal(response.body.jwks_uri, "https://customissuer/jwks"); - }); -}); diff --git a/test/openid/override.test.ts b/test/openid/override.test.ts new file mode 100644 index 000000000..0610b1043 --- /dev/null +++ b/test/openid/override.test.ts @@ -0,0 +1,146 @@ +import assert from 'assert' +import express from 'express' +import request from 'supertest' + +import { afterAll, beforeEach, describe, it } from 'vitest' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import OpenIdRecipe from 'supertokens-node/recipe/openid/recipe' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`overrideTest: ${printPath('[test/openid/override.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test overriding open id functions', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + OpenIdRecipe.init({ + issuer: 'https://api.supertokens.io/auth', + override: { + functions(oi) { + return { + ...oi, + getOpenIdDiscoveryConfiguration() { + return { + issuer: 'https://customissuer', + jwks_uri: 'https://customissuer/jwks', + } + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise((resolve) => { + request(app) + .get('/auth/.well-known/openid-configuration') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) + + assert(response.body !== undefined) + assert.equal(response.body.issuer, 'https://customissuer') + assert.equal(response.body.jwks_uri, 'https://customissuer/jwks') + }) + + it('Test overriding open id apis', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + OpenIdRecipe.init({ + issuer: 'https://api.supertokens.io/auth', + override: { + apis(oi) { + return { + ...oi, + getOpenIdDiscoveryConfigurationGET({ options }) { + return { + status: 'OK', + issuer: 'https://customissuer', + jwks_uri: 'https://customissuer/jwks', + } + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise((resolve) => { + request(app) + .get('/auth/.well-known/openid-configuration') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) + + assert(response.body !== undefined) + assert.equal(response.body.issuer, 'https://customissuer') + assert.equal(response.body.jwks_uri, 'https://customissuer/jwks') + }) +}) diff --git a/test/passwordless/apis.test.js b/test/passwordless/apis.test.js deleted file mode 100644 index 43e2ddf5f..000000000 --- a/test/passwordless/apis.test.js +++ /dev/null @@ -1,1536 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig, stopST } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let Passwordless = require("../../recipe/passwordless"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let SuperTokens = require("../../lib/build/supertokens").default; -const request = require("supertest"); -const express = require("express"); -let { middleware, errorHandler } = require("../../framework/express"); -let { isCDIVersionCompatible } = require("../utils"); - -describe(`apisFunctions: ${printPath("[test/passwordless/apis.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - finish full sign up / in flow with email (create code -> consume code) - */ - - it("test the sign up /in flow with email using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - createAndSendCustomEmail: (input) => { - userInputCode = input.userInputCode; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with email - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - - // consumeCode API - let validUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: validCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 3); - assert(Object.keys(validUserInputCodeResponse).length === 3); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - finish full sign up / in flow with phoneNumber (create code -> consume code) - */ - - it("test the sign up /in flow with phoneNumber using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - userInputCode = input.userInputCode; - return; - }, - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with phoneNumber - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - - // consumeCode API - let validUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: validCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.phoneNumber === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 3); - assert(Object.keys(validUserInputCodeResponse).length === 3); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - create code with email and then resend code and make sure that sending email function is called while resending code - */ - it("test creating a code with email and then resending the code and check that the sending custom email function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomEmailCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - createAndSendCustomEmail: (input) => { - isCreateAndSendCustomEmailCalled = true; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with email - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(isCreateAndSendCustomEmailCalled); - - isCreateAndSendCustomEmailCalled = false; - - // resendCode API - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: validCreateCodeResponse.deviceId, - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - assert(isCreateAndSendCustomEmailCalled); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - create code with phone and then resend code and make sure that sending SMS function is called while resending code - */ - it("test creating a code with phone and then resending the code and check that the sending custom SMS function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomTextMessageCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - isCreateAndSendCustomTextMessageCalled = true; - return; - }, - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with email - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(isCreateAndSendCustomTextMessageCalled); - - isCreateAndSendCustomTextMessageCalled = false; - - // resendCode API - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: validCreateCodeResponse.deviceId, - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - assert(isCreateAndSendCustomTextMessageCalled); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - sending both email and phone in createCode API throws bad request - * - sending neither email and phone in createCode API throws bad request - */ - it("test invalid input to createCodeAPI while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // sending both email and phone in createCode API throws bad request - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - email: "test@example.com", - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide exactly one of email or phoneNumber"); - } - - { - // sending neither email and phone in createCode API throws bad request - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide exactly one of email or phoneNumber"); - } - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - do full sign in with email, then manually add a user's phone to their user Info, then so sign in with that phone number and make sure that the same userId signs in. - - */ - - it("test adding phoneNumber to a users info and signing in will sign in the same user, using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let userInputCode = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - userInputCode = input.userInputCode; - return; - }, - createAndSendCustomEmail: (input) => { - userInputCode = input.userInputCode; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // create a passwordless user with email - let emailCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(emailCreateCodeResponse.status === "OK"); - - // consumeCode API - let emailUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: emailCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: emailCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(emailUserInputCodeResponse.status === "OK"); - - // add users phoneNumber to userInfo - await Passwordless.updateUser({ - userId: emailUserInputCodeResponse.user.id, - phoneNumber: "+12345678901", - }); - - // sign in user with phone numbers - let phoneCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(phoneCreateCodeResponse.status === "OK"); - - let phoneUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: phoneCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: phoneCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(phoneUserInputCodeResponse.status === "OK"); - - // check that the same user has signed in - assert(phoneUserInputCodeResponse.user.id === emailUserInputCodeResponse.user.id); - }); - - // check that if user has not given linkCode nor (deviceId+userInputCode), it throws a bad request error. - it("test not passing any fields to consumeCodeAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // dont send linkCode or (deviceId+userInputCode) - let badResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: "sessionId", - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(badResponse.res.statusMessage === "Bad Request"); - assert( - JSON.parse(badResponse.text).message === - "Please provide one of (linkCode) or (deviceId+userInputCode) and not both" - ); - } - }); - - it("test consumeCodeAPI with magic link", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - { - // send an invalid linkCode - let letInvalidLinkCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: "invalidLinkCode", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(letInvalidLinkCodeResponse.status === "RESTART_FLOW_ERROR"); - } - - { - // send a valid linkCode - let validLinkCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validLinkCodeResponse.status === "OK"); - assert(validLinkCodeResponse.createdNewUser === true); - assert(typeof validLinkCodeResponse.user.id === "string"); - assert(typeof validLinkCodeResponse.user.email === "string"); - assert(typeof validLinkCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validLinkCodeResponse.user).length === 3); - assert(Object.keys(validLinkCodeResponse).length === 3); - } - }); - - it("test consumeCodeAPI with code", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: () => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - { - // send an incorrect userInputCode - let incorrectUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: "invalidLinkCode", - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(incorrectUserInputCodeResponse.status === "INCORRECT_USER_INPUT_CODE_ERROR"); - assert(incorrectUserInputCodeResponse.failedCodeInputAttemptCount === 1); - //checking default value for maximumCodeInputAttempts is 5 - assert(incorrectUserInputCodeResponse.maximumCodeInputAttempts === 5); - assert(Object.keys(incorrectUserInputCodeResponse).length === 3); - } - - { - // send a valid userInputCode - let validUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 3); - assert(Object.keys(validUserInputCodeResponse).length === 3); - } - - { - // send a used userInputCode - let usedUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(usedUserInputCodeResponse.status === "RESTART_FLOW_ERROR"); - } - }); - - it("test consumeCodeAPI with expired code", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - await new Promise((r) => setTimeout(r, 2000)); // wait for code to expire - let expiredUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(expiredUserInputCodeResponse.status === "EXPIRED_USER_INPUT_CODE_ERROR"); - assert(expiredUserInputCodeResponse.failedCodeInputAttemptCount === 1); - //checking default value for maximumCodeInputAttempts is 5 - assert(expiredUserInputCodeResponse.maximumCodeInputAttempts === 5); - assert(Object.keys(expiredUserInputCodeResponse).length === 3); - } - }); - - it("test createCodeAPI with email", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // passing valid field - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - } - - { - // passing invalid email - let invalidEmailCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "invalidEmail", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(invalidEmailCreateCodeResponse.status === "GENERAL_ERROR"); - assert(invalidEmailCreateCodeResponse.message === "Email is invalid"); - } - }); - - it("test createCodeAPI with phoneNumber", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // passing valid field - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - } - - { - // passing invalid phoneNumber - let invalidPhoneNumberCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "123", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(invalidPhoneNumberCreateCodeResponse.status === "GENERAL_ERROR"); - assert(invalidPhoneNumberCreateCodeResponse.message === "Phone number is invalid"); - } - }); - - it("test magicLink format in createCodeAPI", async function () { - await startST(); - - let magicLinkURL = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - magicLinkURL = new URL(input.urlWithLinkCode); - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // passing valid field - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validCreateCodeResponse.status === "OK"); - - // check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode - assert(magicLinkURL.hostname === "supertokens.io"); - assert(magicLinkURL.pathname === "/auth/verify"); - assert(magicLinkURL.searchParams.get("rid") === "passwordless"); - assert(magicLinkURL.searchParams.get("preAuthSessionId") === validCreateCodeResponse.preAuthSessionId); - assert(magicLinkURL.hash.length > 1); - } - }); - - it("test emailExistsAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // email does not exist - let emailDoesNotExistResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(emailDoesNotExistResponse.status === "OK"); - assert(emailDoesNotExistResponse.exists === false); - } - - { - // email exists - - // create a passwordless user through email - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - await Passwordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - }); - - let emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(emailExistsResponse.status === "OK"); - assert(emailExistsResponse.exists === true); - } - }); - - it("test phoneNumberExistsAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // phoneNumber does not exist - let phoneNumberDoesNotExistResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/phonenumber/exists") - .query({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(phoneNumberDoesNotExistResponse.status === "OK"); - assert(phoneNumberDoesNotExistResponse.exists === false); - } - - { - // phoneNumber exists - - // create a passwordless user through phone - let codeInfo = await Passwordless.createCode({ - phoneNumber: "+1234567890", - }); - - await Passwordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - }); - - let phoneNumberExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/phonenumber/exists") - .query({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(phoneNumberExistsResponse.status === "OK"); - assert(phoneNumberExistsResponse.exists === true); - } - }); - - //resendCode API - - it("test resendCodeAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - let codeInfo = await Passwordless.createCode({ - phoneNumber: "+1234567890", - }); - - // valid response - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: codeInfo.deviceId, - preAuthSessionId: codeInfo.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - } - - { - // invalid preAuthSessionId and deviceId - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: "TU/52WOcktSv99zqaAZuWJG9BSoS0aRLfCbep8rFEwk=", - preAuthSessionId: "kFmkPQEAJtACiT2w/K8fndEuNm+XozJXSZSlWEr+iGs=", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "RESTART_FLOW_ERROR"); - } - }); - - // test that you create a code with PHONE in config, you then change the config to use EMAIL, you call resendCode API, it should return RESTART_FLOW_ERROR - it("test resendCodeAPI when changing contact method", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - let codeInfo = await Passwordless.createCode({ - phoneNumber: "+1234567890", - }); - - await killAllST(); - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - { - // invalid preAuthSessionId and deviceId - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: codeInfo.deviceId, - preAuthSessionId: codeInfo.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "RESTART_FLOW_ERROR"); - } - } - }); -}); diff --git a/test/passwordless/apis.test.ts b/test/passwordless/apis.test.ts new file mode 100644 index 000000000..4e3833411 --- /dev/null +++ b/test/passwordless/apis.test.ts @@ -0,0 +1,1495 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import Passwordless from 'supertokens-node/recipe/passwordless' +import { ProcessState } from 'supertokens-node/processState' +import request from 'supertest' +import express from 'express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, isCDIVersionCompatible, killAllST, printPath, setKeyValueInConfig, setupST, startST } from '../utils' + +describe(`apisFunctions: ${printPath('[test/passwordless/apis.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - finish full sign up / in flow with email (create code -> consume code) + */ + + it('test the sign up /in flow with email using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with email + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + + // consumeCode API + const validUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: validCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validUserInputCodeResponse.status === 'OK') + assert(validUserInputCodeResponse.createdNewUser === true) + assert(typeof validUserInputCodeResponse.user.id === 'string') + assert(typeof validUserInputCodeResponse.user.email === 'string') + assert(typeof validUserInputCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validUserInputCodeResponse.user).length === 3) + assert(Object.keys(validUserInputCodeResponse).length === 3) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - finish full sign up / in flow with phoneNumber (create code -> consume code) + */ + + it('test the sign up /in flow with phoneNumber using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + userInputCode = input.userInputCode + }, + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with phoneNumber + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + + // consumeCode API + const validUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: validCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validUserInputCodeResponse.status === 'OK') + assert(validUserInputCodeResponse.createdNewUser === true) + assert(typeof validUserInputCodeResponse.user.id === 'string') + assert(typeof validUserInputCodeResponse.user.phoneNumber === 'string') + assert(typeof validUserInputCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validUserInputCodeResponse.user).length === 3) + assert(Object.keys(validUserInputCodeResponse).length === 3) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - create code with email and then resend code and make sure that sending email function is called while resending code + */ + it('test creating a code with email and then resending the code and check that the sending custom email function is called while using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomEmailCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + createAndSendCustomEmail: (input) => { + isCreateAndSendCustomEmailCalled = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with email + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(isCreateAndSendCustomEmailCalled) + + isCreateAndSendCustomEmailCalled = false + + // resendCode API + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: validCreateCodeResponse.deviceId, + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + assert(isCreateAndSendCustomEmailCalled) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - create code with phone and then resend code and make sure that sending SMS function is called while resending code + */ + it('test creating a code with phone and then resending the code and check that the sending custom SMS function is called while using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomTextMessageCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + isCreateAndSendCustomTextMessageCalled = true + }, + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with email + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(isCreateAndSendCustomTextMessageCalled) + + isCreateAndSendCustomTextMessageCalled = false + + // resendCode API + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: validCreateCodeResponse.deviceId, + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + assert(isCreateAndSendCustomTextMessageCalled) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - sending both email and phone in createCode API throws bad request + * - sending neither email and phone in createCode API throws bad request + */ + it('test invalid input to createCodeAPI while using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // sending both email and phone in createCode API throws bad request + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + email: 'test@example.com', + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide exactly one of email or phoneNumber') + } + + { + // sending neither email and phone in createCode API throws bad request + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide exactly one of email or phoneNumber') + } + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - do full sign in with email, then manually add a user's phone to their user Info, then so sign in with that phone number and make sure that the same userId signs in. + + */ + + it('test adding phoneNumber to a users info and signing in will sign in the same user, using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let userInputCode + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + userInputCode = input.userInputCode + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // create a passwordless user with email + const emailCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(emailCreateCodeResponse.status === 'OK') + + // consumeCode API + const emailUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: emailCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: emailCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(emailUserInputCodeResponse.status === 'OK') + + // add users phoneNumber to userInfo + await Passwordless.updateUser({ + userId: emailUserInputCodeResponse.user.id, + phoneNumber: '+12345678901', + }) + + // sign in user with phone numbers + const phoneCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(phoneCreateCodeResponse.status === 'OK') + + const phoneUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: phoneCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: phoneCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(phoneUserInputCodeResponse.status === 'OK') + + // check that the same user has signed in + assert(phoneUserInputCodeResponse.user.id === emailUserInputCodeResponse.user.id) + }) + + // check that if user has not given linkCode nor (deviceId+userInputCode), it throws a bad request error. + it('test not passing any fields to consumeCodeAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // dont send linkCode or (deviceId+userInputCode) + const badResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: 'sessionId', + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert(badResponse.res.statusMessage === 'Bad Request') + assert( + JSON.parse(badResponse.text).message + === 'Please provide one of (linkCode) or (deviceId+userInputCode) and not both', + ) + } + }) + + it('test consumeCodeAPI with magic link', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + { + // send an invalid linkCode + const letInvalidLinkCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: 'invalidLinkCode', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(letInvalidLinkCodeResponse.status === 'RESTART_FLOW_ERROR') + } + + { + // send a valid linkCode + const validLinkCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validLinkCodeResponse.status === 'OK') + assert(validLinkCodeResponse.createdNewUser === true) + assert(typeof validLinkCodeResponse.user.id === 'string') + assert(typeof validLinkCodeResponse.user.email === 'string') + assert(typeof validLinkCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validLinkCodeResponse.user).length === 3) + assert(Object.keys(validLinkCodeResponse).length === 3) + } + }) + + it('test consumeCodeAPI with code', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: () => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + { + // send an incorrect userInputCode + const incorrectUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: 'invalidLinkCode', + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(incorrectUserInputCodeResponse.status === 'INCORRECT_USER_INPUT_CODE_ERROR') + assert(incorrectUserInputCodeResponse.failedCodeInputAttemptCount === 1) + // checking default value for maximumCodeInputAttempts is 5 + assert(incorrectUserInputCodeResponse.maximumCodeInputAttempts === 5) + assert(Object.keys(incorrectUserInputCodeResponse).length === 3) + } + + { + // send a valid userInputCode + const validUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validUserInputCodeResponse.status === 'OK') + assert(validUserInputCodeResponse.createdNewUser === true) + assert(typeof validUserInputCodeResponse.user.id === 'string') + assert(typeof validUserInputCodeResponse.user.email === 'string') + assert(typeof validUserInputCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validUserInputCodeResponse.user).length === 3) + assert(Object.keys(validUserInputCodeResponse).length === 3) + } + + { + // send a used userInputCode + const usedUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(usedUserInputCodeResponse.status === 'RESTART_FLOW_ERROR') + } + }) + + it('test consumeCodeAPI with expired code', async () => { + await setKeyValueInConfig('passwordless_code_lifetime', 1000) // one second lifetime + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + await new Promise(r => setTimeout(r, 2000)) // wait for code to expire + const expiredUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(expiredUserInputCodeResponse.status === 'EXPIRED_USER_INPUT_CODE_ERROR') + assert(expiredUserInputCodeResponse.failedCodeInputAttemptCount === 1) + // checking default value for maximumCodeInputAttempts is 5 + assert(expiredUserInputCodeResponse.maximumCodeInputAttempts === 5) + assert(Object.keys(expiredUserInputCodeResponse).length === 3) + } + }) + + it('test createCodeAPI with email', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // passing valid field + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + } + + { + // passing invalid email + const invalidEmailCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'invalidEmail', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(invalidEmailCreateCodeResponse.status === 'GENERAL_ERROR') + assert(invalidEmailCreateCodeResponse.message === 'Email is invalid') + } + }) + + it('test createCodeAPI with phoneNumber', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // passing valid field + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + } + + { + // passing invalid phoneNumber + const invalidPhoneNumberCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '123', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(invalidPhoneNumberCreateCodeResponse.status === 'GENERAL_ERROR') + assert(invalidPhoneNumberCreateCodeResponse.message === 'Phone number is invalid') + } + }) + + it('test magicLink format in createCodeAPI', async () => { + await startST() + + let magicLinkURL + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + magicLinkURL = new URL(input.urlWithLinkCode) + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // passing valid field + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validCreateCodeResponse.status === 'OK') + + // check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode + assert(magicLinkURL.hostname === 'supertokens.io') + assert(magicLinkURL.pathname === '/auth/verify') + assert(magicLinkURL.searchParams.get('rid') === 'passwordless') + assert(magicLinkURL.searchParams.get('preAuthSessionId') === validCreateCodeResponse.preAuthSessionId) + assert(magicLinkURL.hash.length > 1) + } + }) + + it('test emailExistsAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // email does not exist + const emailDoesNotExistResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(emailDoesNotExistResponse.status === 'OK') + assert(emailDoesNotExistResponse.exists === false) + } + + { + // email exists + + // create a passwordless user through email + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + await Passwordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + }) + + const emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(emailExistsResponse.status === 'OK') + assert(emailExistsResponse.exists === true) + } + }) + + it('test phoneNumberExistsAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // phoneNumber does not exist + const phoneNumberDoesNotExistResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/phonenumber/exists') + .query({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(phoneNumberDoesNotExistResponse.status === 'OK') + assert(phoneNumberDoesNotExistResponse.exists === false) + } + + { + // phoneNumber exists + + // create a passwordless user through phone + const codeInfo = await Passwordless.createCode({ + phoneNumber: '+1234567890', + }) + + await Passwordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + }) + + const phoneNumberExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/phonenumber/exists') + .query({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(phoneNumberExistsResponse.status === 'OK') + assert(phoneNumberExistsResponse.exists === true) + } + }) + + // resendCode API + + it('test resendCodeAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + const codeInfo = await Passwordless.createCode({ + phoneNumber: '+1234567890', + }) + + // valid response + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: codeInfo.deviceId, + preAuthSessionId: codeInfo.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + } + + { + // invalid preAuthSessionId and deviceId + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: 'TU/52WOcktSv99zqaAZuWJG9BSoS0aRLfCbep8rFEwk=', + preAuthSessionId: 'kFmkPQEAJtACiT2w/K8fndEuNm+XozJXSZSlWEr+iGs=', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'RESTART_FLOW_ERROR') + } + }) + + // test that you create a code with PHONE in config, you then change the config to use EMAIL, you call resendCode API, it should return RESTART_FLOW_ERROR + it('test resendCodeAPI when changing contact method', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + const codeInfo = await Passwordless.createCode({ + phoneNumber: '+1234567890', + }) + + await killAllST() + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + { + // invalid preAuthSessionId and deviceId + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: codeInfo.deviceId, + preAuthSessionId: codeInfo.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'RESTART_FLOW_ERROR') + } + } + }) +}) diff --git a/test/passwordless/config.test.js b/test/passwordless/config.test.js deleted file mode 100644 index 16085d55e..000000000 --- a/test/passwordless/config.test.js +++ /dev/null @@ -1,1494 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig, stopST } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let Passwordless = require("../../recipe/passwordless"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let SuperTokens = require("../../lib/build/supertokens").default; -const request = require("supertest"); -const express = require("express"); -let { middleware, errorHandler } = require("../../framework/express"); -let { isCDIVersionCompatible, generateRandomCode } = require("../utils"); -let PasswordlessRecipe = require("../../lib/build/recipe/passwordless/recipe").default; - -describe(`config tests: ${printPath("[test/passwordless/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - /* - contactMethod: EMAIL_OR_PHONE - - minimal config - */ - it("test minimum config with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let passwordlessRecipe = await PasswordlessRecipe.getInstanceOrThrowError(); - assert(passwordlessRecipe.config.contactMethod === "EMAIL_OR_PHONE"); - assert(passwordlessRecipe.config.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - }); - - /* - contactMethod: EMAIL_OR_PHONE - - adding custom validators for phone and email and making sure that they are called - */ - it("test adding custom validators for phone and email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isValidateEmailAddressCalled = false; - let isValidatePhoneNumberCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - validateEmailAddress: (email) => { - isValidateEmailAddressCalled = true; - return undefined; - }, - validatePhoneNumber: (phoneNumber) => { - isValidatePhoneNumberCalled = true; - return undefined; - }, - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // check if validatePhoneNumber is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidatePhoneNumberCalled); - assert(response.status === "OK"); - } - - { - // check if validateEmailAddress is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidateEmailAddressCalled); - assert(response.status === "OK"); - } - }); - - /** - * - with contactMethod EMAIL_OR_PHONE - * - adding custom functions to send email and making sure that is called - */ - - it("test custom function to send email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomEmailCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - isCreateAndSendCustomEmailCalled = true; - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // check if createAndSendCustomEmail is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isCreateAndSendCustomEmailCalled); - assert(response.status === "OK"); - } - }); - - /** - * - with contactMethod EMAIL_OR_PHONE - * - adding custom functions to send text SMS, and making sure that is called - * - */ - - it("test custom function to send text SMS with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomTextMessageCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - isCreateAndSendCustomTextMessageCalled = true; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // check if createAndSendCustomTextMessage is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isCreateAndSendCustomTextMessageCalled); - assert(response.status === "OK"); - } - }); - - /* - contactMethod: PHONE - - minimal input works - */ - it("test minimum config with phone contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let passwordlessRecipe = await PasswordlessRecipe.getInstanceOrThrowError(); - assert(passwordlessRecipe.config.contactMethod === "PHONE"); - assert(passwordlessRecipe.config.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - }); - - /* contactMethod: PHONE - If passed validatePhoneNumber, it gets called when the createCode API is called - - If you return undefined from the function, the API works. - - If you return a string from the function, the API throws a GENERIC ERROR - */ - - it("test if validatePhoneNumber is called with phone contactMethod", async function () { - await startST(); - - let isValidatePhoneNumberCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - validatePhoneNumber: (phoneNumber) => { - isValidatePhoneNumberCalled = true; - return undefined; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // If you return undefined from the function, the API works - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidatePhoneNumberCalled); - assert(response.status === "OK"); - } - - { - // If you return a string from the function, the API throws a GENERIC ERROR - - await killAllST(); - await startST(); - - isValidatePhoneNumberCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - validatePhoneNumber: (phoneNumber) => { - isValidatePhoneNumberCalled = true; - return "test error"; - }, - }), - ], - }); - - { - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidatePhoneNumberCalled); - assert(response.status === "GENERAL_ERROR"); - assert(response.message === "test error"); - } - } - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined - - check all other inputs to this function are as expected - */ - - it("test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE and phone contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - let isOtherInputValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE", - createAndSendCustomTextMessage: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - - if ( - typeof input.codeLifetime === "number" && - typeof input.phoneNumber === "string" && - typeof input.preAuthSessionId === "string" - ) { - isOtherInputValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - assert(isOtherInputValid); - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined - */ - - it("test createAndSendCustomTextMessage with flowType: MAGIC_LINK and phone contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined - */ - it("test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and phone contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - if you throw an error from this function, it should contain a general error in the response - */ - - it("test createAndSendCustomTextMessage, if error is thrown, it should contain a general error in the response", async function () { - await startST(); - - let isCreateAndSendCustomTextMessageCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - isCreateAndSendCustomTextMessageCalled = true; - throw new Error("test message"); - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(500) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(message === "test message"); - assert(isCreateAndSendCustomTextMessageCalled); - }); - - /* - - contactMethod: EMAIL - - minimal input works - */ - - it("test minimum config with email contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let passwordlessRecipe = await PasswordlessRecipe.getInstanceOrThrowError(); - assert(passwordlessRecipe.config.contactMethod === "EMAIL"); - assert(passwordlessRecipe.config.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - }); - - /* - - contactMethod: EMAIL - - if passed validateEmailAddress, it gets called when the createCode API is called - - If you return undefined from the function, the API works. - - If you return a string from the function, the API throws a GENERIC ERROR - */ - - it("test if validateEmailAddress is called with email contactMethod", async function () { - await startST(); - - let isValidateEmailAddressCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - validateEmailAddress: (email) => { - isValidateEmailAddressCalled = true; - return undefined; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // If you return undefined from the function, the API works - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidateEmailAddressCalled); - assert(response.status === "OK"); - } - - { - // If you return a string from the function, the API throws a GENERIC ERROR - - await killAllST(); - await startST(); - - isValidateEmailAddressCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - validateEmailAddress: (email) => { - isValidateEmailAddressCalled = true; - return "test error"; - }, - }), - ], - }); - - { - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidateEmailAddressCalled); - assert(response.status === "GENERAL_ERROR"); - assert(response.message === "test error"); - } - } - }); - - /* - - contactMethod: EMAIL - - if passed createAndSendCustomEmail, it gets called with the right inputs: - - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined - - check all other inputs to this function are as expected - */ - - it("test createAndSendCustomEmail with flowType: USER_INPUT_CODE and email contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - let isOtherInputValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - createAndSendCustomEmail: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - - if ( - typeof input.codeLifetime === "number" && - typeof input.email === "string" && - typeof input.preAuthSessionId === "string" - ) { - isOtherInputValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - assert(isOtherInputValid); - }); - - /* contactMethod: EMAIL - If passed createAndSendCustomEmail, it gets called with the right inputs: - - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined - */ - - it("test createAndSendCustomEmail with flowType: MAGIC_LINK and email contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "MAGIC_LINK", - createAndSendCustomEmail: (input) => { - if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: EMAIL - If passed createAndSendCustomEmail, it gets called with the right inputs: - - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined - */ - it("test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and email contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: EMAIL - If passed createAndSendCustomEmail, it gets called with the right inputs: - - if you throw an error from this function, the status in the response should be a general error - */ - - it("test createAndSendCustomEmail, if error is thrown, the status in the response should be a general error", async function () { - await startST(); - - let isCreateAndSendCustomEmailCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "MAGIC_LINK", - createAndSendCustomEmail: (input) => { - isCreateAndSendCustomEmailCalled = true; - throw new Error("test message"); - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(500) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(message === "test message"); - assert(isCreateAndSendCustomEmailCalled); - }); - - /* - - Missing compulsory configs throws as error: - - flowType is necessary, contactMethod is necessary - */ - - it("test missing compulsory configs throws an error", async function () { - await startST(); - - { - // missing flowType - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - }), - ], - }); - assert(false); - } catch (err) { - if (err.message !== "Please pass flowType argument in the config") { - throw err; - } - } - } - - { - await killAllST(); - await startST(); - - // missing contactMethod - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - flowType: "USER_INPUT_CODE", - }), - ], - }); - assert(false); - } catch (err) { - if (err.message !== `Please pass one of "PHONE", "EMAIL" or "EMAIL_OR_PHONE" as the contactMethod`) { - throw err; - } - } - } - }); - - /* - - Passing getCustomUserInputCode: - - Check that it is called when the createCode and resendCode APIs are called - - Check that the result returned from this are actually what the user input code is - - Check that is you return the same code everytime from this function and call resendCode API, you get USER_INPUT_CODE_ALREADY_USED_ERROR output from the API - */ - - it("test passing getCustomUserInputCode using different codes", async function () { - await startST(); - - let customCode = undefined; - let userCodeSent = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - getCustomUserInputCode: (input) => { - customCode = generateRandomCode(5); - return customCode; - }, - createAndSendCustomEmail: (input) => { - userCodeSent = input.userInputCode; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let createCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(createCodeResponse.status === "OK"); - - assert(userCodeSent === customCode); - - customCode = undefined; - userCodeSent = undefined; - - let resendUserCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: createCodeResponse.deviceId, - preAuthSessionId: createCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(resendUserCodeResponse.status === "OK"); - assert(userCodeSent === customCode); - }); - - it("test passing getCustomUserInputCode using the same code", async function () { - await startST(); - - // using the same customCode - let customCode = "customCode"; - let userCodeSent = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - getCustomUserInputCode: (input) => { - return customCode; - }, - createAndSendCustomEmail: (input) => { - userCodeSent = input.userInputCode; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let createCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(createCodeResponse.status === "OK"); - assert(userCodeSent === customCode); - - let createNewCodeForDeviceResponse = await Passwordless.createNewCodeForDevice({ - deviceId: createCodeResponse.deviceId, - userInputCode: customCode, - }); - - assert(createNewCodeForDeviceResponse.status === "USER_INPUT_CODE_ALREADY_USED_ERROR"); - - let resendUserCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: createCodeResponse.deviceId, - preAuthSessionId: createCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(resendUserCodeResponse.status === "GENERAL_ERROR"); - assert(resendUserCodeResponse.message === "Failed to generate a one time code. Please try again"); - }); - - // Check basic override usage - it("test basic override usage in passwordless", async function () { - await startST(); - - let customDeviceId = "customDeviceId"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - createAndSendCustomEmail: (input) => { - return; - }, - override: { - apis: (oI) => { - return { - ...oI, - createCodePOST: async (input) => { - let response = await oI.createCodePOST(input); - response.deviceId = customDeviceId; - return response; - }, - }; - }, - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let createCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(createCodeResponse.deviceId === customDeviceId); - }); -}); diff --git a/test/passwordless/config.test.ts b/test/passwordless/config.test.ts new file mode 100644 index 000000000..9373e98c0 --- /dev/null +++ b/test/passwordless/config.test.ts @@ -0,0 +1,1457 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import Passwordless from 'supertokens-node/recipe/passwordless' +import { ProcessState } from 'supertokens-node/processState' +import request from 'supertest' +import express from 'express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import PasswordlessRecipe from 'supertokens-node/recipe/passwordless/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, generateRandomCode, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`config tests: ${printPath('[test/passwordless/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + /* + contactMethod: EMAIL_OR_PHONE + - minimal config + */ + it('test minimum config with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const passwordlessRecipe = await PasswordlessRecipe.getInstanceOrThrowError() + assert(passwordlessRecipe.config.contactMethod === 'EMAIL_OR_PHONE') + assert(passwordlessRecipe.config.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + }) + + /* + contactMethod: EMAIL_OR_PHONE + - adding custom validators for phone and email and making sure that they are called + */ + it('test adding custom validators for phone and email with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isValidateEmailAddressCalled = false + let isValidatePhoneNumberCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + validateEmailAddress: (email) => { + isValidateEmailAddressCalled = true + return undefined + }, + validatePhoneNumber: (phoneNumber) => { + isValidatePhoneNumberCalled = true + return undefined + }, + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // check if validatePhoneNumber is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidatePhoneNumberCalled) + assert(response.status === 'OK') + } + + { + // check if validateEmailAddress is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidateEmailAddressCalled) + assert(response.status === 'OK') + } + }) + + /** + * - with contactMethod EMAIL_OR_PHONE + * - adding custom functions to send email and making sure that is called + */ + + it('test custom function to send email with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomEmailCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + isCreateAndSendCustomEmailCalled = true + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // check if createAndSendCustomEmail is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isCreateAndSendCustomEmailCalled) + assert(response.status === 'OK') + } + }) + + /** + * - with contactMethod EMAIL_OR_PHONE + * - adding custom functions to send text SMS, and making sure that is called + * + */ + + it('test custom function to send text SMS with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomTextMessageCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + isCreateAndSendCustomTextMessageCalled = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // check if createAndSendCustomTextMessage is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isCreateAndSendCustomTextMessageCalled) + assert(response.status === 'OK') + } + }) + + /* + contactMethod: PHONE + - minimal input works + */ + it('test minimum config with phone contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const passwordlessRecipe = await PasswordlessRecipe.getInstanceOrThrowError() + assert(passwordlessRecipe.config.contactMethod === 'PHONE') + assert(passwordlessRecipe.config.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + }) + + /* contactMethod: PHONE + If passed validatePhoneNumber, it gets called when the createCode API is called + - If you return undefined from the function, the API works. + - If you return a string from the function, the API throws a GENERIC ERROR + */ + + it('test if validatePhoneNumber is called with phone contactMethod', async () => { + await startST() + + let isValidatePhoneNumberCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + validatePhoneNumber: (phoneNumber) => { + isValidatePhoneNumberCalled = true + return undefined + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // If you return undefined from the function, the API works + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidatePhoneNumberCalled) + assert(response.status === 'OK') + } + + { + // If you return a string from the function, the API throws a GENERIC ERROR + + await killAllST() + await startST() + + isValidatePhoneNumberCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + validatePhoneNumber: (phoneNumber) => { + isValidatePhoneNumberCalled = true + return 'test error' + }, + }), + ], + }) + + { + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidatePhoneNumberCalled) + assert(response.status === 'GENERAL_ERROR') + assert(response.message === 'test error') + } + } + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined + - check all other inputs to this function are as expected + */ + + it('test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE and phone contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + let isOtherInputValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE', + createAndSendCustomTextMessage: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + + if ( + typeof input.codeLifetime === 'number' + && typeof input.phoneNumber === 'string' + && typeof input.preAuthSessionId === 'string' + ) + isOtherInputValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + assert(isOtherInputValid) + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined + */ + + it('test createAndSendCustomTextMessage with flowType: MAGIC_LINK and phone contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined + */ + it('test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and phone contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - if you throw an error from this function, it should contain a general error in the response + */ + + it('test createAndSendCustomTextMessage, if error is thrown, it should contain a general error in the response', async () => { + await startST() + + let isCreateAndSendCustomTextMessageCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + isCreateAndSendCustomTextMessageCalled = true + throw new Error('test message') + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(500) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(message === 'test message') + assert(isCreateAndSendCustomTextMessageCalled) + }) + + /* + - contactMethod: EMAIL + - minimal input works + */ + + it('test minimum config with email contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const passwordlessRecipe = await PasswordlessRecipe.getInstanceOrThrowError() + assert(passwordlessRecipe.config.contactMethod === 'EMAIL') + assert(passwordlessRecipe.config.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + }) + + /* + - contactMethod: EMAIL + - if passed validateEmailAddress, it gets called when the createCode API is called + - If you return undefined from the function, the API works. + - If you return a string from the function, the API throws a GENERIC ERROR + */ + + it('test if validateEmailAddress is called with email contactMethod', async () => { + await startST() + + let isValidateEmailAddressCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + validateEmailAddress: (email) => { + isValidateEmailAddressCalled = true + return undefined + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // If you return undefined from the function, the API works + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidateEmailAddressCalled) + assert(response.status === 'OK') + } + + { + // If you return a string from the function, the API throws a GENERIC ERROR + + await killAllST() + await startST() + + isValidateEmailAddressCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + validateEmailAddress: (email) => { + isValidateEmailAddressCalled = true + return 'test error' + }, + }), + ], + }) + + { + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidateEmailAddressCalled) + assert(response.status === 'GENERAL_ERROR') + assert(response.message === 'test error') + } + } + }) + + /* + - contactMethod: EMAIL + - if passed createAndSendCustomEmail, it gets called with the right inputs: + - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined + - check all other inputs to this function are as expected + */ + + it('test createAndSendCustomEmail with flowType: USER_INPUT_CODE and email contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + let isOtherInputValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + createAndSendCustomEmail: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + + if ( + typeof input.codeLifetime === 'number' + && typeof input.email === 'string' + && typeof input.preAuthSessionId === 'string' + ) + isOtherInputValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + assert(isOtherInputValid) + }) + + /* contactMethod: EMAIL + If passed createAndSendCustomEmail, it gets called with the right inputs: + - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined + */ + + it('test createAndSendCustomEmail with flowType: MAGIC_LINK and email contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'MAGIC_LINK', + createAndSendCustomEmail: (input) => { + if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: EMAIL + If passed createAndSendCustomEmail, it gets called with the right inputs: + - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined + */ + it('test createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and email contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: EMAIL + If passed createAndSendCustomEmail, it gets called with the right inputs: + - if you throw an error from this function, the status in the response should be a general error + */ + + it('test createAndSendCustomEmail, if error is thrown, the status in the response should be a general error', async () => { + await startST() + + let isCreateAndSendCustomEmailCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'MAGIC_LINK', + createAndSendCustomEmail: (input) => { + isCreateAndSendCustomEmailCalled = true + throw new Error('test message') + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(500) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(message === 'test message') + assert(isCreateAndSendCustomEmailCalled) + }) + + /* + - Missing compulsory configs throws as error: + - flowType is necessary, contactMethod is necessary + */ + + it('test missing compulsory configs throws an error', async () => { + await startST() + + { + // missing flowType + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + }), + ], + }) + assert(false) + } + catch (err) { + if (err.message !== 'Please pass flowType argument in the config') + throw err + } + } + + { + await killAllST() + await startST() + + // missing contactMethod + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + flowType: 'USER_INPUT_CODE', + }), + ], + }) + assert(false) + } + catch (err) { + if (err.message !== 'Please pass one of "PHONE", "EMAIL" or "EMAIL_OR_PHONE" as the contactMethod') + throw err + } + } + }) + + /* + - Passing getCustomUserInputCode: + - Check that it is called when the createCode and resendCode APIs are called + - Check that the result returned from this are actually what the user input code is + - Check that is you return the same code everytime from this function and call resendCode API, you get USER_INPUT_CODE_ALREADY_USED_ERROR output from the API + */ + + it('test passing getCustomUserInputCode using different codes', async () => { + await startST() + + let customCode + let userCodeSent + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + getCustomUserInputCode: (input) => { + customCode = generateRandomCode(5) + return customCode + }, + createAndSendCustomEmail: (input) => { + userCodeSent = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const createCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(createCodeResponse.status === 'OK') + + assert(userCodeSent === customCode) + + customCode = undefined + userCodeSent = undefined + + const resendUserCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: createCodeResponse.deviceId, + preAuthSessionId: createCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(resendUserCodeResponse.status === 'OK') + assert(userCodeSent === customCode) + }) + + it('test passing getCustomUserInputCode using the same code', async () => { + await startST() + + // using the same customCode + const customCode = 'customCode' + let userCodeSent + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + getCustomUserInputCode: (input) => { + return customCode + }, + createAndSendCustomEmail: (input) => { + userCodeSent = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const createCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(createCodeResponse.status === 'OK') + assert(userCodeSent === customCode) + + const createNewCodeForDeviceResponse = await Passwordless.createNewCodeForDevice({ + deviceId: createCodeResponse.deviceId, + userInputCode: customCode, + }) + + assert(createNewCodeForDeviceResponse.status === 'USER_INPUT_CODE_ALREADY_USED_ERROR') + + const resendUserCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: createCodeResponse.deviceId, + preAuthSessionId: createCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(resendUserCodeResponse.status === 'GENERAL_ERROR') + assert(resendUserCodeResponse.message === 'Failed to generate a one time code. Please try again') + }) + + // Check basic override usage + it('test basic override usage in passwordless', async () => { + await startST() + + const customDeviceId = 'customDeviceId' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + createAndSendCustomEmail: (input) => { + + }, + override: { + apis: (oI) => { + return { + ...oI, + createCodePOST: async (input) => { + const response = await oI.createCodePOST(input) + response.deviceId = customDeviceId + return response + }, + } + }, + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const createCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(createCodeResponse.deviceId === customDeviceId) + }) +}) diff --git a/test/passwordless/emailDelivery.test.js b/test/passwordless/emailDelivery.test.js deleted file mode 100644 index 17b957ad4..000000000 --- a/test/passwordless/emailDelivery.test.js +++ /dev/null @@ -1,918 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, delay } = require("../utils"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let Passwordless = require("../../recipe/passwordless"); -let { SMTPService } = require("../../recipe/passwordless/emaildelivery"); -let nock = require("nock"); -let supertest = require("supertest"); -const { middleware, errorHandler } = require("../../framework/express"); -let express = require("express"); -let { isCDIVersionCompatible } = require("../utils"); - -describe(`emailDelivery: ${printPath("[test/passwordless/emailDelivery.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await cleanST(); - }); - - it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: passwordless login", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: async (input) => { - email = input.email; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: passwordless login", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - email = input.email; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test smtp service: passwordless login", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let urlWithLinkCode = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - sendRawEmailCalled = true; - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.subject, urlWithLinkCode); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - userInputCode = input.userInputCode; - urlWithLinkCode = input.urlWithLinkCode; - codeLifetime = input.codeLifetime; - return { - body: input.userInputCode, - toEmail: input.email, - subject: input.urlWithLinkCode, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); - - it("test default backward compatibility api being called: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: resend code api", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let sendCustomEmailCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendCustomEmailCalled) { - email = input.email; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - } - sendCustomEmailCalled = true; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - await delay(2); - assert.strictEqual(sendCustomEmailCalled, true); - assert.strictEqual(email, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: resend code api", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - let overrideCalled = false; - let loginCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (overrideCalled) { - email = input.email; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - } - overrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - assert.strictEqual(overrideCalled, true); - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test smtp service: resend code api", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendRawEmailCalled) { - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.subject, urlWithLinkCode); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - } - sendRawEmailCalled = true; - }, - getContent: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (getContentCalled) { - userInputCode = input.userInputCode; - urlWithLinkCode = input.urlWithLinkCode; - codeLifetime = input.codeLifetime; - } - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - return { - body: input.userInputCode, - toEmail: input.email, - subject: input.urlWithLinkCode, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - await delay(2); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.strictEqual(email, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - await delay(2); - - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - assert.strictEqual(appName, undefined); - assert.strictEqual(email, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - let result = await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); -}); diff --git a/test/passwordless/emailDelivery.test.ts b/test/passwordless/emailDelivery.test.ts new file mode 100644 index 000000000..79a757f77 --- /dev/null +++ b/test/passwordless/emailDelivery.test.ts @@ -0,0 +1,909 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import Passwordless from 'supertokens-node/recipe/passwordless' +import { SMTPService } from 'supertokens-node/recipe/passwordless/emaildelivery' +import nock from 'nock' +import supertest from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import express from 'express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, delay, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`emailDelivery: ${printPath('[test/passwordless/emailDelivery.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await cleanST() + }) + + it('test default backward compatibility api being called: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: passwordless login', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: async (input) => { + email = input.email + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: passwordless login', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + email = input.email + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test smtp service: passwordless login', async () => { + await startST() + let email + let codeLifetime + let userInputCode + let urlWithLinkCode + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + sendRawEmailCalled = true + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.subject, urlWithLinkCode) + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + userInputCode = input.userInputCode + urlWithLinkCode = input.urlWithLinkCode + codeLifetime = input.codeLifetime + return { + body: input.userInputCode, + toEmail: input.email, + subject: input.urlWithLinkCode, + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) + + it('test default backward compatibility api being called: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: resend code api', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let sendCustomEmailCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendCustomEmailCalled) { + email = input.email + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + } + sendCustomEmailCalled = true + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + await delay(2) + assert.strictEqual(sendCustomEmailCalled, true) + assert.strictEqual(email, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: resend code api', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + let overrideCalled = false + let loginCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (overrideCalled) { + email = input.email + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + } + overrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + assert.strictEqual(overrideCalled, true) + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test smtp service: resend code api', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendRawEmailCalled) { + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.subject, urlWithLinkCode) + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + } + sendRawEmailCalled = true + }, + getContent: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (getContentCalled) { + userInputCode = input.userInputCode + urlWithLinkCode = input.urlWithLinkCode + codeLifetime = input.codeLifetime + } + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + return { + body: input.userInputCode, + toEmail: input.email, + subject: input.urlWithLinkCode, + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + await delay(2) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.strictEqual(email, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + await delay(2) + + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + assert.strictEqual(appName, undefined) + assert.strictEqual(email, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + const result = await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) +}) diff --git a/test/passwordless/recipeFunctions.test.js b/test/passwordless/recipeFunctions.test.js deleted file mode 100644 index dbd0393b6..000000000 --- a/test/passwordless/recipeFunctions.test.js +++ /dev/null @@ -1,942 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let Passwordless = require("../../recipe/passwordless"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let SuperTokens = require("../../lib/build/supertokens").default; -let { isCDIVersionCompatible } = require("../utils"); - -describe(`recipeFunctions: ${printPath("[test/passwordless/recipeFunctions.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("getUser test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let user = await Passwordless.getUserById({ - userId: "random", - }); - - assert(user === undefined); - - user = ( - await Passwordless.signInUp({ - email: "test@example.com", - }) - ).user; - - let result = await Passwordless.getUserById({ - userId: user.id, - }); - - assert(result.id === user.id); - assert(result.email !== undefined && user.email === result.email); - assert(result.phoneNumber === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 3); - } - - { - let user = await Passwordless.getUserByEmail({ - email: "random", - }); - - assert(user === undefined); - - user = ( - await Passwordless.signInUp({ - email: "test@example.com", - }) - ).user; - - let result = await Passwordless.getUserByEmail({ - email: user.email, - }); - - assert(result.id === user.id); - assert(result.email !== undefined && user.email === result.email); - assert(result.phoneNumber === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 3); - } - - { - let user = await Passwordless.getUserByPhoneNumber({ - phoneNumber: "random", - }); - - assert(user === undefined); - - user = ( - await Passwordless.signInUp({ - phoneNumber: "+1234567890", - }) - ).user; - - let result = await Passwordless.getUserByPhoneNumber({ - phoneNumber: user.phoneNumber, - }); - - assert(result.id === user.id); - assert(result.phoneNumber !== undefined && user.phoneNumber === result.phoneNumber); - assert(result.email === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 3); - } - }); - - it("createCode test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let resp = await Passwordless.createCode({ - email: "test@example.com", - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - - { - let resp = await Passwordless.createCode({ - email: "test@example.com", - userInputCode: "123", - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - }); - - it("createNewCodeForDevice test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let resp = await Passwordless.createCode({ - email: "test@example.com", - }); - - resp = await Passwordless.createNewCodeForDevice({ - deviceId: resp.deviceId, - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - - { - let resp = await Passwordless.createCode({ - email: "test@example.com", - }); - - resp = await Passwordless.createNewCodeForDevice({ - deviceId: resp.deviceId, - userInputCode: "1234", - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - - { - let resp = await Passwordless.createCode({ - email: "test@example.com", - }); - - resp = await Passwordless.createNewCodeForDevice({ - deviceId: "random", - }); - - assert(resp.status === "RESTART_FLOW_ERROR"); - assert(Object.keys(resp).length === 1); - } - - { - let resp = await Passwordless.createCode({ - email: "test@example.com", - userInputCode: "1234", - }); - - resp = await Passwordless.createNewCodeForDevice({ - deviceId: resp.deviceId, - userInputCode: "1234", - }); - - assert(resp.status === "USER_INPUT_CODE_ALREADY_USED_ERROR"); - assert(Object.keys(resp).length === 1); - } - }); - - it("consumeCode test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - let resp = await Passwordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert(resp.status === "OK"); - assert(resp.createdNewUser); - assert(typeof resp.user.id === "string"); - assert(resp.user.email === "test@example.com"); - assert(resp.user.phoneNumber === undefined); - assert(typeof resp.user.timeJoined === "number"); - assert(Object.keys(resp).length === 3); - assert(Object.keys(resp.user).length === 3); - } - - { - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - let resp = await Passwordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: "random", - deviceId: codeInfo.deviceId, - }); - - assert(resp.status === "INCORRECT_USER_INPUT_CODE_ERROR"); - assert(resp.failedCodeInputAttemptCount === 1); - assert(resp.maximumCodeInputAttempts === 5); - assert(Object.keys(resp).length === 3); - } - - { - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - try { - await Passwordless.consumeCode({ - preAuthSessionId: "random", - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - assert(false); - } catch (err) { - assert(err.message.includes("preAuthSessionId and deviceId doesn't match")); - } - } - }); - - it("consumeCode test with EXPIRED_USER_INPUT_CODE_ERROR", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let codeInfo = await Passwordless.createCode({ - email: "test@example.com", - }); - - await new Promise((r) => setTimeout(r, 2000)); // wait for code to expire - - let resp = await Passwordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert(resp.status === "EXPIRED_USER_INPUT_CODE_ERROR"); - assert(resp.failedCodeInputAttemptCount === 1); - assert(resp.maximumCodeInputAttempts === 5); - assert(Object.keys(resp).length === 3); - } - }); - - // updateUser - it("updateUser contactMethod email test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let userInfo = await Passwordless.signInUp({ - email: "test@example.com", - }); - - { - // update users email - let response = await Passwordless.updateUser({ - userId: userInfo.user.id, - email: "test2@example.com", - }); - assert(response.status === "OK"); - - let result = await Passwordless.getUserById({ - userId: userInfo.user.id, - }); - - assert(result.email === "test2@example.com"); - } - { - // update user with invalid userId - let response = await Passwordless.updateUser({ - userId: "invalidUserId", - email: "test2@example.com", - }); - assert(response.status === "UNKNOWN_USER_ID_ERROR"); - } - { - // update user with an email that already exists - let userInfo2 = await Passwordless.signInUp({ - email: "test3@example.com", - }); - - let result = await Passwordless.updateUser({ - userId: userInfo2.user.id, - email: "test2@example.com", - }); - - assert(result.status === "EMAIL_ALREADY_EXISTS_ERROR"); - } - }); - - it("updateUser contactMethod phone test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let phoneNumber_1 = "+1234567891"; - let phoneNumber_2 = "+1234567892"; - let phoneNumber_3 = "+1234567893"; - - let userInfo = await Passwordless.signInUp({ - phoneNumber: phoneNumber_1, - }); - - { - // update users email - let response = await Passwordless.updateUser({ - userId: userInfo.user.id, - phoneNumber: phoneNumber_2, - }); - assert(response.status === "OK"); - - let result = await Passwordless.getUserById({ - userId: userInfo.user.id, - }); - - assert(result.phoneNumber === phoneNumber_2); - } - { - // update user with a phoneNumber that already exists - let userInfo2 = await Passwordless.signInUp({ - phoneNumber: phoneNumber_3, - }); - - let result = await Passwordless.updateUser({ - userId: userInfo2.user.id, - phoneNumber: phoneNumber_2, - }); - - assert(result.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR"); - } - }); - - // revokeAllCodes - it("revokeAllCodes test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await Passwordless.createCode({ - email: "test@example.com", - }); - - let codeInfo_2 = await Passwordless.createCode({ - email: "test@example.com", - }); - - { - let result = await Passwordless.revokeAllCodes({ - email: "test@example.com", - }); - - assert(result.status === "OK"); - } - - { - let result_1 = await Passwordless.consumeCode({ - preAuthSessionId: codeInfo_1.preAuthSessionId, - deviceId: codeInfo_1.deviceId, - userInputCode: codeInfo_1.userInputCode, - }); - - assert(result_1.status === "RESTART_FLOW_ERROR"); - - let result_2 = await Passwordless.consumeCode({ - preAuthSessionId: codeInfo_2.preAuthSessionId, - deviceId: codeInfo_2.deviceId, - userInputCode: codeInfo_2.userInputCode, - }); - - assert(result_2.status === "RESTART_FLOW_ERROR"); - } - }); - - it("revokeCode test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await Passwordless.createCode({ - email: "test@example.com", - }); - - let codeInfo_2 = await Passwordless.createCode({ - email: "test@example.com", - }); - - { - let result = await Passwordless.revokeCode({ - codeId: codeInfo_1.codeId, - }); - - assert(result.status === "OK"); - } - - { - let result_1 = await Passwordless.consumeCode({ - preAuthSessionId: codeInfo_1.preAuthSessionId, - deviceId: codeInfo_1.deviceId, - userInputCode: codeInfo_1.userInputCode, - }); - - assert(result_1.status === "RESTART_FLOW_ERROR"); - - let result_2 = await Passwordless.consumeCode({ - preAuthSessionId: codeInfo_2.preAuthSessionId, - deviceId: codeInfo_2.deviceId, - userInputCode: codeInfo_2.userInputCode, - }); - - assert(result_2.status === "OK"); - } - }); - - // listCodesByEmail - it("listCodesByEmail test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await Passwordless.createCode({ - email: "test@example.com", - }); - - let codeInfo_2 = await Passwordless.createCode({ - email: "test@example.com", - }); - - let result = await Passwordless.listCodesByEmail({ - email: "test@example.com", - }); - assert(result.length === 2); - result.forEach((element) => { - element.codes.forEach((code) => { - if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) { - assert(false); - } - }); - }); - }); - - //listCodesByPhoneNumber - it("listCodesByPhoneNumber test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await Passwordless.createCode({ - phoneNumber: "+1234567890", - }); - - let codeInfo_2 = await Passwordless.createCode({ - phoneNumber: "+1234567890", - }); - - let result = await Passwordless.listCodesByPhoneNumber({ - phoneNumber: "+1234567890", - }); - assert(result.length === 2); - result.forEach((element) => { - element.codes.forEach((code) => { - if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) { - assert(false); - } - }); - }); - }); - - // listCodesByDeviceId and listCodesByPreAuthSessionId - it("listCodesByDeviceId and listCodesByPreAuthSessionId test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await Passwordless.createCode({ - phoneNumber: "+1234567890", - }); - - { - let result = await Passwordless.listCodesByDeviceId({ - deviceId: codeInfo_1.deviceId, - }); - assert(result.codes[0].codeId === codeInfo_1.codeId); - } - - { - let result = await Passwordless.listCodesByPreAuthSessionId({ - preAuthSessionId: codeInfo_1.preAuthSessionId, - }); - assert(result.codes[0].codeId === codeInfo_1.codeId); - } - }); - - /* - - createMagicLink - - check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode - */ - - it("createMagicLink test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let result = await Passwordless.createMagicLink({ - phoneNumber: "+1234567890", - }); - - let magicLinkURL = new URL(result); - - assert(magicLinkURL.hostname === "supertokens.io"); - assert(magicLinkURL.pathname === "/auth/verify"); - assert(magicLinkURL.searchParams.get("rid") === "passwordless"); - assert(typeof magicLinkURL.searchParams.get("preAuthSessionId") === "string"); - assert(magicLinkURL.hash.length > 1); - }); - - // signInUp test - it("signInUp test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let result = await Passwordless.signInUp({ - phoneNumber: "+12345678901", - }); - - assert(result.status === "OK"); - assert(result.createdNewUser === true); - assert(Object.keys(result).length === 3); - - assert(result.user.phoneNumber === "+12345678901"); - assert(typeof result.user.id === "string"); - assert(typeof result.user.timeJoined === "number"); - assert(Object.keys(result.user).length === 3); - }); -}); diff --git a/test/passwordless/recipeFunctions.test.ts b/test/passwordless/recipeFunctions.test.ts new file mode 100644 index 000000000..1e893e81d --- /dev/null +++ b/test/passwordless/recipeFunctions.test.ts @@ -0,0 +1,927 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import Passwordless from 'supertokens-node/recipe/passwordless' +import { ProcessState } from 'supertokens-node/processState' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, isCDIVersionCompatible, killAllST, printPath, setKeyValueInConfig, setupST, startST } from '../utils' + +describe(`recipeFunctions: ${printPath('[test/passwordless/recipeFunctions.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('getUser test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + let user = await Passwordless.getUserById({ + userId: 'random', + }) + + assert(user === undefined) + + user = ( + await Passwordless.signInUp({ + email: 'test@example.com', + }) + ).user + + const result = await Passwordless.getUserById({ + userId: user.id, + }) + + assert(result.id === user.id) + assert(result.email !== undefined && user.email === result.email) + assert(result.phoneNumber === undefined) + assert(typeof result.timeJoined === 'number') + assert(Object.keys(result).length === 3) + } + + { + let user = await Passwordless.getUserByEmail({ + email: 'random', + }) + + assert(user === undefined) + + user = ( + await Passwordless.signInUp({ + email: 'test@example.com', + }) + ).user + + const result = await Passwordless.getUserByEmail({ + email: user.email, + }) + + assert(result.id === user.id) + assert(result.email !== undefined && user.email === result.email) + assert(result.phoneNumber === undefined) + assert(typeof result.timeJoined === 'number') + assert(Object.keys(result).length === 3) + } + + { + let user = await Passwordless.getUserByPhoneNumber({ + phoneNumber: 'random', + }) + + assert(user === undefined) + + user = ( + await Passwordless.signInUp({ + phoneNumber: '+1234567890', + }) + ).user + + const result = await Passwordless.getUserByPhoneNumber({ + phoneNumber: user.phoneNumber, + }) + + assert(result.id === user.id) + assert(result.phoneNumber !== undefined && user.phoneNumber === result.phoneNumber) + assert(result.email === undefined) + assert(typeof result.timeJoined === 'number') + assert(Object.keys(result).length === 3) + } + }) + + it('createCode test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + const resp = await Passwordless.createCode({ + email: 'test@example.com', + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + + { + const resp = await Passwordless.createCode({ + email: 'test@example.com', + userInputCode: '123', + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + }) + + it('createNewCodeForDevice test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + let resp = await Passwordless.createCode({ + email: 'test@example.com', + }) + + resp = await Passwordless.createNewCodeForDevice({ + deviceId: resp.deviceId, + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + + { + let resp = await Passwordless.createCode({ + email: 'test@example.com', + }) + + resp = await Passwordless.createNewCodeForDevice({ + deviceId: resp.deviceId, + userInputCode: '1234', + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + + { + let resp = await Passwordless.createCode({ + email: 'test@example.com', + }) + + resp = await Passwordless.createNewCodeForDevice({ + deviceId: 'random', + }) + + assert(resp.status === 'RESTART_FLOW_ERROR') + assert(Object.keys(resp).length === 1) + } + + { + let resp = await Passwordless.createCode({ + email: 'test@example.com', + userInputCode: '1234', + }) + + resp = await Passwordless.createNewCodeForDevice({ + deviceId: resp.deviceId, + userInputCode: '1234', + }) + + assert(resp.status === 'USER_INPUT_CODE_ALREADY_USED_ERROR') + assert(Object.keys(resp).length === 1) + } + }) + + it('consumeCode test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + const resp = await Passwordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert(resp.status === 'OK') + assert(resp.createdNewUser) + assert(typeof resp.user.id === 'string') + assert(resp.user.email === 'test@example.com') + assert(resp.user.phoneNumber === undefined) + assert(typeof resp.user.timeJoined === 'number') + assert(Object.keys(resp).length === 3) + assert(Object.keys(resp.user).length === 3) + } + + { + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + const resp = await Passwordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: 'random', + deviceId: codeInfo.deviceId, + }) + + assert(resp.status === 'INCORRECT_USER_INPUT_CODE_ERROR') + assert(resp.failedCodeInputAttemptCount === 1) + assert(resp.maximumCodeInputAttempts === 5) + assert(Object.keys(resp).length === 3) + } + + { + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + try { + await Passwordless.consumeCode({ + preAuthSessionId: 'random', + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + assert(false) + } + catch (err) { + assert(err.message.includes('preAuthSessionId and deviceId doesn\'t match')) + } + } + }) + + it('consumeCode test with EXPIRED_USER_INPUT_CODE_ERROR', async () => { + await setKeyValueInConfig('passwordless_code_lifetime', 1000) // one second lifetime + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + const codeInfo = await Passwordless.createCode({ + email: 'test@example.com', + }) + + await new Promise(r => setTimeout(r, 2000)) // wait for code to expire + + const resp = await Passwordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert(resp.status === 'EXPIRED_USER_INPUT_CODE_ERROR') + assert(resp.failedCodeInputAttemptCount === 1) + assert(resp.maximumCodeInputAttempts === 5) + assert(Object.keys(resp).length === 3) + } + }) + + // updateUser + it('updateUser contactMethod email test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const userInfo = await Passwordless.signInUp({ + email: 'test@example.com', + }) + + { + // update users email + const response = await Passwordless.updateUser({ + userId: userInfo.user.id, + email: 'test2@example.com', + }) + assert(response.status === 'OK') + + const result = await Passwordless.getUserById({ + userId: userInfo.user.id, + }) + + assert(result.email === 'test2@example.com') + } + { + // update user with invalid userId + const response = await Passwordless.updateUser({ + userId: 'invalidUserId', + email: 'test2@example.com', + }) + assert(response.status === 'UNKNOWN_USER_ID_ERROR') + } + { + // update user with an email that already exists + const userInfo2 = await Passwordless.signInUp({ + email: 'test3@example.com', + }) + + const result = await Passwordless.updateUser({ + userId: userInfo2.user.id, + email: 'test2@example.com', + }) + + assert(result.status === 'EMAIL_ALREADY_EXISTS_ERROR') + } + }) + + it('updateUser contactMethod phone test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const phoneNumber_1 = '+1234567891' + const phoneNumber_2 = '+1234567892' + const phoneNumber_3 = '+1234567893' + + const userInfo = await Passwordless.signInUp({ + phoneNumber: phoneNumber_1, + }) + + { + // update users email + const response = await Passwordless.updateUser({ + userId: userInfo.user.id, + phoneNumber: phoneNumber_2, + }) + assert(response.status === 'OK') + + const result = await Passwordless.getUserById({ + userId: userInfo.user.id, + }) + + assert(result.phoneNumber === phoneNumber_2) + } + { + // update user with a phoneNumber that already exists + const userInfo2 = await Passwordless.signInUp({ + phoneNumber: phoneNumber_3, + }) + + const result = await Passwordless.updateUser({ + userId: userInfo2.user.id, + phoneNumber: phoneNumber_2, + }) + + assert(result.status === 'PHONE_NUMBER_ALREADY_EXISTS_ERROR') + } + }) + + // revokeAllCodes + it('revokeAllCodes test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await Passwordless.createCode({ + email: 'test@example.com', + }) + + const codeInfo_2 = await Passwordless.createCode({ + email: 'test@example.com', + }) + + { + const result = await Passwordless.revokeAllCodes({ + email: 'test@example.com', + }) + + assert(result.status === 'OK') + } + + { + const result_1 = await Passwordless.consumeCode({ + preAuthSessionId: codeInfo_1.preAuthSessionId, + deviceId: codeInfo_1.deviceId, + userInputCode: codeInfo_1.userInputCode, + }) + + assert(result_1.status === 'RESTART_FLOW_ERROR') + + const result_2 = await Passwordless.consumeCode({ + preAuthSessionId: codeInfo_2.preAuthSessionId, + deviceId: codeInfo_2.deviceId, + userInputCode: codeInfo_2.userInputCode, + }) + + assert(result_2.status === 'RESTART_FLOW_ERROR') + } + }) + + it('revokeCode test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await Passwordless.createCode({ + email: 'test@example.com', + }) + + const codeInfo_2 = await Passwordless.createCode({ + email: 'test@example.com', + }) + + { + const result = await Passwordless.revokeCode({ + codeId: codeInfo_1.codeId, + }) + + assert(result.status === 'OK') + } + + { + const result_1 = await Passwordless.consumeCode({ + preAuthSessionId: codeInfo_1.preAuthSessionId, + deviceId: codeInfo_1.deviceId, + userInputCode: codeInfo_1.userInputCode, + }) + + assert(result_1.status === 'RESTART_FLOW_ERROR') + + const result_2 = await Passwordless.consumeCode({ + preAuthSessionId: codeInfo_2.preAuthSessionId, + deviceId: codeInfo_2.deviceId, + userInputCode: codeInfo_2.userInputCode, + }) + + assert(result_2.status === 'OK') + } + }) + + // listCodesByEmail + it('listCodesByEmail test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await Passwordless.createCode({ + email: 'test@example.com', + }) + + const codeInfo_2 = await Passwordless.createCode({ + email: 'test@example.com', + }) + + const result = await Passwordless.listCodesByEmail({ + email: 'test@example.com', + }) + assert(result.length === 2) + result.forEach((element) => { + element.codes.forEach((code) => { + if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) + assert(false) + }) + }) + }) + + // listCodesByPhoneNumber + it('listCodesByPhoneNumber test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await Passwordless.createCode({ + phoneNumber: '+1234567890', + }) + + const codeInfo_2 = await Passwordless.createCode({ + phoneNumber: '+1234567890', + }) + + const result = await Passwordless.listCodesByPhoneNumber({ + phoneNumber: '+1234567890', + }) + assert(result.length === 2) + result.forEach((element) => { + element.codes.forEach((code) => { + if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) + assert(false) + }) + }) + }) + + // listCodesByDeviceId and listCodesByPreAuthSessionId + it('listCodesByDeviceId and listCodesByPreAuthSessionId test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await Passwordless.createCode({ + phoneNumber: '+1234567890', + }) + + { + const result = await Passwordless.listCodesByDeviceId({ + deviceId: codeInfo_1.deviceId, + }) + assert(result.codes[0].codeId === codeInfo_1.codeId) + } + + { + const result = await Passwordless.listCodesByPreAuthSessionId({ + preAuthSessionId: codeInfo_1.preAuthSessionId, + }) + assert(result.codes[0].codeId === codeInfo_1.codeId) + } + }) + + /* + - createMagicLink + - check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode + */ + + it('createMagicLink test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const result = await Passwordless.createMagicLink({ + phoneNumber: '+1234567890', + }) + + const magicLinkURL = new URL(result) + + assert(magicLinkURL.hostname === 'supertokens.io') + assert(magicLinkURL.pathname === '/auth/verify') + assert(magicLinkURL.searchParams.get('rid') === 'passwordless') + assert(typeof magicLinkURL.searchParams.get('preAuthSessionId') === 'string') + assert(magicLinkURL.hash.length > 1) + }) + + // signInUp test + it('signInUp test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const result = await Passwordless.signInUp({ + phoneNumber: '+12345678901', + }) + + assert(result.status === 'OK') + assert(result.createdNewUser === true) + assert(Object.keys(result).length === 3) + + assert(result.user.phoneNumber === '+12345678901') + assert(typeof result.user.id === 'string') + assert(typeof result.user.timeJoined === 'number') + assert(Object.keys(result.user).length === 3) + }) +}) diff --git a/test/passwordless/smsDelivery.test.js b/test/passwordless/smsDelivery.test.js deleted file mode 100644 index 5a5afe333..000000000 --- a/test/passwordless/smsDelivery.test.js +++ /dev/null @@ -1,1276 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, delay } = require("../utils"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let Passwordless = require("../../recipe/passwordless"); -let { TwilioService, SupertokensService } = require("../../recipe/passwordless/smsdelivery"); -let nock = require("nock"); -let supertest = require("supertest"); -const { middleware, errorHandler } = require("../../framework/express"); -let express = require("express"); -let { isCDIVersionCompatible } = require("../utils"); - -describe(`smsDelivery: ${printPath("[test/passwordless/smsDelivery.test.js]")}`, function () { - beforeEach(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await cleanST(); - }); - - it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let apiKey = "randomKey"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert.strictEqual(apiKey, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: async (input) => { - phoneNumber = input.phoneNumber; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - override: (oI) => { - return { - sendSms: async (input) => { - phoneNumber = input.phoneNumber; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test twilio service: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let sendRawSmsCalled = false; - let getContentCalled = false; - let twilioAPICalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new TwilioService({ - twilioSettings: { - accountSid: "ACTWILIO_ACCOUNT_SID", - authToken: "test-token", - from: "+919909909999", - }, - override: (oI) => { - return { - sendRawSms: async (input) => { - sendRawSmsCalled = true; - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.toPhoneNumber, "+919909909998"); - phoneNumber = input.toPhoneNumber; - await oI.sendRawSms(input); - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - return { - body: input.userInputCode, - toPhoneNumber: input.phoneNumber, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.twilio.com/2010-04-01") - .post("/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json") - .reply(200, (uri, body) => { - twilioAPICalled = true; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert(outerOverrideCalled); - assert(sendRawSmsCalled); - assert(getContentCalled); - assert(twilioAPICalled); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(500, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); - - it("test supertokens service: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let supertokensAPICalled = false; - let apiKey = undefined; - let type = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new SupertokensService("API_KEY"), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - supertokensAPICalled = true; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - type = body.smsInput.type; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert(outerOverrideCalled); - assert(supertokensAPICalled); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(apiKey, "API_KEY"); - }); - - it("test default backward compatibility api being called, error message not sent back to user if response code is 429: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(429, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.body.status, "OK"); - }); - - it("test default backward compatibility api being called: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - let apiKey = "randomKey"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert.strictEqual(apiKey, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let sendCustomSMSCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendCustomSMSCalled) { - phoneNumber = input.phoneNumber; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - } - sendCustomSMSCalled = true; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - await delay(2); - assert.strictEqual(sendCustomSMSCalled, true); - assert.strictEqual(phoneNumber, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - let overrideCalled = false; - let loginCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - override: (oI) => { - return { - sendSms: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (overrideCalled) { - phoneNumber = input.phoneNumber; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - } - overrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - assert.strictEqual(overrideCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test twilio service: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let urlWithLinkCode = undefined; - let outerOverrideCalled = false; - let sendRawSmsCalled = false; - let getContentCalled = false; - let loginCalled = false; - let twilioAPICalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new TwilioService({ - twilioSettings: { - accountSid: "ACTWILIO_ACCOUNT_SID", - authToken: "test-token", - from: "+919909909999", - }, - override: (oI) => { - return { - sendRawSms: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendRawSmsCalled) { - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.toPhoneNumber, "+919909909998"); - phoneNumber = input.toPhoneNumber; - } - sendRawSmsCalled = true; - await oI.sendRawSms(input); - }, - getContent: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (getContentCalled) { - userInputCode = input.userInputCode; - urlWithLinkCode = input.urlWithLinkCode; - codeLifetime = input.codeLifetime; - } - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - getContentCalled = true; - return { - body: input.userInputCode, - toPhoneNumber: input.phoneNumber, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.twilio.com/2010-04-01") - .post("/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert(getContentCalled); - assert(sendRawSmsCalled); - assert(loginCalled); - assert.strictEqual(phoneNumber, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - nock("https://api.twilio.com/2010-04-01") - .post("/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json") - .reply(200, (uri, body) => { - twilioAPICalled = true; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert(outerOverrideCalled); - assert(twilioAPICalled); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - assert.strictEqual(appName, undefined); - assert.strictEqual(phoneNumber, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(500, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - let result = await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); - - it("test supertokens service: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let supertokensAPICalled = false; - let apiKey = undefined; - let type = undefined; - let loginCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new SupertokensService("API_KEY"), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert(loginCalled); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - supertokensAPICalled = true; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - type = body.smsInput.type; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - await delay(2); - - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert(outerOverrideCalled); - assert(supertokensAPICalled); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(apiKey, "API_KEY"); - }); - - it("test default backward compatibility api being called, error message not sent back to user if response code is 429: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "passwordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(429, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - let result = await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "passwordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.body.status, "OK"); - }); -}); diff --git a/test/passwordless/smsDelivery.test.ts b/test/passwordless/smsDelivery.test.ts new file mode 100644 index 000000000..0a1374562 --- /dev/null +++ b/test/passwordless/smsDelivery.test.ts @@ -0,0 +1,1263 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import Passwordless from 'supertokens-node/recipe/passwordless' +import { SupertokensService, TwilioService } from 'supertokens-node/recipe/passwordless/smsdelivery' +import nock from 'nock' +import supertest from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import express from 'express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, delay, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`smsDelivery: ${printPath('[test/passwordless/smsDelivery.test.ts]')}`, () => { + beforeEach(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await cleanST() + }) + + it('test default backward compatibility api being called: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let apiKey = 'randomKey' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert.strictEqual(apiKey, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: async (input) => { + phoneNumber = input.phoneNumber + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + override: (oI) => { + return { + sendSms: async (input) => { + phoneNumber = input.phoneNumber + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test twilio service: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let outerOverrideCalled = false + let sendRawSmsCalled = false + let getContentCalled = false + let twilioAPICalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new TwilioService({ + twilioSettings: { + accountSid: 'ACTWILIO_ACCOUNT_SID', + authToken: 'test-token', + from: '+919909909999', + }, + override: (oI) => { + return { + sendRawSms: async (input) => { + sendRawSmsCalled = true + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.toPhoneNumber, '+919909909998') + phoneNumber = input.toPhoneNumber + await oI.sendRawSms(input) + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + return { + body: input.userInputCode, + toPhoneNumber: input.phoneNumber, + } + }, + } + }, + }), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.twilio.com/2010-04-01') + .post('/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json') + .reply(200, (uri, body) => { + twilioAPICalled = true + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert(outerOverrideCalled) + assert(sendRawSmsCalled) + assert(getContentCalled) + assert(twilioAPICalled) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(500, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) + + it('test supertokens service: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let outerOverrideCalled = false + let supertokensAPICalled = false + let apiKey + let type + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new SupertokensService('API_KEY'), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + supertokensAPICalled = true + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + type = body.smsInput.type + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert(outerOverrideCalled) + assert(supertokensAPICalled) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(apiKey, 'API_KEY') + }) + + it('test default backward compatibility api being called, error message not sent back to user if response code is 429: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(429, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.body.status, 'OK') + }) + + it('test default backward compatibility api being called: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + let apiKey = 'randomKey' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert.strictEqual(apiKey, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let sendCustomSMSCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendCustomSMSCalled) { + phoneNumber = input.phoneNumber + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + } + sendCustomSMSCalled = true + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + await delay(2) + assert.strictEqual(sendCustomSMSCalled, true) + assert.strictEqual(phoneNumber, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + let overrideCalled = false + let loginCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + override: (oI) => { + return { + sendSms: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (overrideCalled) { + phoneNumber = input.phoneNumber + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + } + overrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + assert.strictEqual(overrideCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test twilio service: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let urlWithLinkCode + let outerOverrideCalled = false + let sendRawSmsCalled = false + let getContentCalled = false + let loginCalled = false + let twilioAPICalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new TwilioService({ + twilioSettings: { + accountSid: 'ACTWILIO_ACCOUNT_SID', + authToken: 'test-token', + from: '+919909909999', + }, + override: (oI) => { + return { + sendRawSms: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendRawSmsCalled) { + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.toPhoneNumber, '+919909909998') + phoneNumber = input.toPhoneNumber + } + sendRawSmsCalled = true + await oI.sendRawSms(input) + }, + getContent: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (getContentCalled) { + userInputCode = input.userInputCode + urlWithLinkCode = input.urlWithLinkCode + codeLifetime = input.codeLifetime + } + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + getContentCalled = true + return { + body: input.userInputCode, + toPhoneNumber: input.phoneNumber, + } + }, + } + }, + }), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.twilio.com/2010-04-01') + .post('/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert(getContentCalled) + assert(sendRawSmsCalled) + assert(loginCalled) + assert.strictEqual(phoneNumber, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + nock('https://api.twilio.com/2010-04-01') + .post('/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json') + .reply(200, (uri, body) => { + twilioAPICalled = true + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert(outerOverrideCalled) + assert(twilioAPICalled) + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + assert.strictEqual(appName, undefined) + assert.strictEqual(phoneNumber, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(500, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + const result = await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) + + it('test supertokens service: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let outerOverrideCalled = false + let supertokensAPICalled = false + let apiKey + let type + let loginCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new SupertokensService('API_KEY'), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert(loginCalled) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + supertokensAPICalled = true + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + type = body.smsInput.type + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + await delay(2) + + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert(outerOverrideCalled) + assert(supertokensAPICalled) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(apiKey, 'API_KEY') + }) + + it('test default backward compatibility api being called, error message not sent back to user if response code is 429: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Passwordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'passwordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(429, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + const result = await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'passwordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.body.status, 'OK') + }) +}) diff --git a/test/querier.test.js b/test/querier.test.js deleted file mode 100644 index fb7852033..000000000 --- a/test/querier.test.js +++ /dev/null @@ -1,364 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig } = require("./utils"); -let ST = require("../"); -let { Querier } = require("../lib/build/querier"); -let assert = require("assert"); -let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); -let Session = require("../recipe/session"); -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -let nock = require("nock"); -const { default: NormalisedURLPath } = require("../lib/build/normalisedURLPath"); -let EmailPassword = require("../recipe/emailpassword"); -let EmailPasswordRecipe = require("../lib/build/recipe/emailpassword/recipe").default; -const { default: axios } = require("axios"); -const { fail } = require("assert"); - -describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // Check that once the API version is there, it doesn't need to query again - it("test that if that once API version is there, it doesn't need to query again", async function () { - await startST(); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - let q = Querier.getNewInstanceOrThrowError(undefined); - await q.getAPIVersion(); - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION, - 2000 - ); - assert(verifyState !== undefined); - - ProcessState.getInstance().reset(); - - await q.getAPIVersion(); - verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION, - 2000 - ); - assert(verifyState === undefined); - }); - - // Check that rid is added to the header iff it's a "/recipe" || "/recipe/*" request. - it("test that rid is added to the header if it's a recipe request", async function () { - await startST(); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let querier = Querier.getNewInstanceOrThrowError(SessionRecipe.getInstanceOrThrowError().getRecipeId()); - - nock("http://localhost:8080", { - allowUnmocked: true, - }) - .get("/recipe") - .reply(200, function (uri, requestBody) { - return this.req.headers; - }); - - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe"), {}); - assert(response.rid === "session"); - - nock("http://localhost:8080", { - allowUnmocked: true, - }) - .get("/recipe/random") - .reply(200, function (uri, requestBody) { - return this.req.headers; - }); - - let response2 = await querier.sendGetRequest(new NormalisedURLPath("/recipe/random"), {}); - assert(response2.rid === "session"); - - nock("http://localhost:8080", { - allowUnmocked: true, - }) - .get("/test") - .reply(200, function (uri, requestBody) { - return this.req.headers; - }); - - let response3 = await querier.sendGetRequest(new NormalisedURLPath("/test"), {}); - assert(response3.rid === undefined); - }); - - it("core not available", async function () { - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080;http://localhost:8081/", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - try { - let q = Querier.getNewInstanceOrThrowError(undefined); - await q.sendGetRequest(new NormalisedURLPath("", "/"), {}); - throw new Error(); - } catch (err) { - if (err.message !== "No SuperTokens core available to query") { - throw err; - } - } - }); - - it("three cores and round robin", async function () { - await startST(); - await startST("localhost", 8081); - await startST("localhost", 8082); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080;http://localhost:8081/;http://localhost:8082", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - let q = Querier.getNewInstanceOrThrowError(undefined); - assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); - assert.equal(await q.sendDeleteRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); - let hostsAlive = q.getHostsAliveForTesting(); - assert.equal(hostsAlive.size, 3); - assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); // this will be the 4th API call - hostsAlive = q.getHostsAliveForTesting(); - assert.equal(hostsAlive.size, 3); - assert.equal(hostsAlive.has("http://localhost:8080"), true); - assert.equal(hostsAlive.has("http://localhost:8081"), true); - assert.equal(hostsAlive.has("http://localhost:8082"), true); - }); - - it("three cores, one dead and round robin", async function () { - await startST(); - await startST("localhost", 8082); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080;http://localhost:8081/;http://localhost:8082", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - let q = Querier.getNewInstanceOrThrowError(undefined); - assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); - assert.equal(await q.sendPostRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); - let hostsAlive = q.getHostsAliveForTesting(); - assert.equal(hostsAlive.size, 2); - assert.equal(await q.sendPutRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); // this will be the 4th API call - hostsAlive = q.getHostsAliveForTesting(); - assert.equal(hostsAlive.size, 2); - assert.equal(hostsAlive.has("http://localhost:8080"), true); - assert.equal(hostsAlive.has("http://localhost:8081"), false); - assert.equal(hostsAlive.has("http://localhost:8082"), true); - }); - - it("test that no connectionURI given, but recipe used throws an error", async function () { - await startST(); - ST.init({ - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - try { - await Session.getSessionInformation(""); - assert(false); - } catch (err) { - assert( - err.message === - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - }); - - it("test that no connectionURI given, recipe override and used doesn't thrown an error", async function () { - await startST(); - ST.init({ - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => { - return { - ...oI, - getSessionInformation: async (input) => { - return input.sessionHandle; - }, - }; - }, - }, - }), - ], - }); - - assert((await Session.getSessionInformation("someHandle")) === "someHandle"); - }); - - it("test with core base path", async function () { - // first we need to know if the core used supports base_path config - await setKeyValueInConfig("base_path", "/test"); - await startST(); - - try { - await axios.get("http://localhost:8080/test/hello"); - } catch (error) { - if (error.response.status === 404) { - //core must be an older version, so we return early - return; - } - throw error; - } - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080/test", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - // we query the core now - let res = await Session.getAllSessionHandlesForUser("user1"); - assert(res.length === 0); - }); - - it("test with incorrect core base path should fail", async function () { - // first we need to know if the core used supports base_path config - await setKeyValueInConfig("base_path", "/some/path"); - await startST(); - - try { - await axios.get("http://localhost:8080/some/path/hello"); - } catch (error) { - if (error.response.status === 404) { - //core must be an older version, so we return early - return; - } - throw error; - } - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080/test", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - try { - // we query the core now - await Session.getAllSessionHandlesForUser("user1"); - fail(); - } catch (err) { - assert(err.message.startsWith("SuperTokens core threw an error")); - } - }); - - it("test with multiple core base path", async function () { - // first we need to know if the core used supports base_path config - await setKeyValueInConfig("base_path", "/some/path"); - await startST(); - - try { - await axios.get("http://localhost:8080/some/path/hello"); - } catch (error) { - if (error.response.status === 404) { - //core must be an older version, so we return early - return; - } - throw error; - } - - await setKeyValueInConfig("base_path", "/test"); - await startST("localhost", 8082); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080/some/path;http://localhost:8082/test", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - { - // we query the first core now - let res = await Session.getAllSessionHandlesForUser("user1"); - assert(res.length === 0); - } - - { - // we query the second core now - let res = await Session.getAllSessionHandlesForUser("user1"); - assert(res.length === 0); - } - }); -}); diff --git a/test/querier.test.ts b/test/querier.test.ts new file mode 100644 index 000000000..e761d7bf5 --- /dev/null +++ b/test/querier.test.ts @@ -0,0 +1,368 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert, { fail } from 'assert' +import ST from 'supertokens-node' +import { Querier } from 'supertokens-node/querier' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import Session from 'supertokens-node/recipe/session' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import nock from 'nock' +import NormalisedURLPath from 'supertokens-node/normalisedURLPath' +import axios from 'axios' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setKeyValueInConfig, setupST, startST } from './utils' + +describe(`Querier: ${printPath('[test/querier.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // Check that once the API version is there, it doesn't need to query again + it('test that if that once API version is there, it doesn\'t need to query again', async () => { + await startST() + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const q = Querier.getNewInstanceOrThrowError(undefined) + await q.getAPIVersion() + + let verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION, + 2000, + ) + assert(verifyState !== undefined) + + ProcessState.getInstance().reset() + + await q.getAPIVersion() + verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_GET_API_VERSION, + 2000, + ) + assert(verifyState === undefined) + }) + + // Check that rid is added to the header iff it's a "/recipe" || "/recipe/*" request. + it('test that rid is added to the header if it\'s a recipe request', async () => { + await startST() + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const querier = Querier.getNewInstanceOrThrowError(SessionRecipe.getInstanceOrThrowError().getRecipeId()) + + nock('http://localhost:8080', { + allowUnmocked: true, + }) + .get('/recipe') + .reply(200, function (uri, requestBody) { + return this.req.headers + }) + + const response = await querier.sendGetRequest(new NormalisedURLPath('/recipe'), {}) + assert(response.rid === 'session') + + nock('http://localhost:8080', { + allowUnmocked: true, + }) + .get('/recipe/random') + .reply(200, function (uri, requestBody) { + return this.req.headers + }) + + const response2 = await querier.sendGetRequest(new NormalisedURLPath('/recipe/random'), {}) + assert(response2.rid === 'session') + + nock('http://localhost:8080', { + allowUnmocked: true, + }) + .get('/test') + .reply(200, function (uri, requestBody) { + return this.req.headers + }) + + const response3 = await querier.sendGetRequest(new NormalisedURLPath('/test'), {}) + assert(response3.rid === undefined) + }) + + it('core not available', async () => { + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080;http://localhost:8081/', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + try { + const q = Querier.getNewInstanceOrThrowError(undefined) + await q.sendGetRequest(new NormalisedURLPath('', '/'), {}) + throw new Error() + } + catch (err) { + if (err.message !== 'No SuperTokens core available to query') + throw err + } + }) + + it('three cores and round robin', async () => { + await startST() + await startST('localhost', 8081) + await startST('localhost', 8082) + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080;http://localhost:8081/;http://localhost:8082', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const q = Querier.getNewInstanceOrThrowError(undefined) + assert.equal(await q.sendGetRequest(new NormalisedURLPath('/hello'), {}), 'Hello\n') + assert.equal(await q.sendDeleteRequest(new NormalisedURLPath('/hello'), {}), 'Hello\n') + let hostsAlive = q.getHostsAliveForTesting() + assert.equal(hostsAlive.size, 3) + assert.equal(await q.sendGetRequest(new NormalisedURLPath('/hello'), {}), 'Hello\n') // this will be the 4th API call + hostsAlive = q.getHostsAliveForTesting() + assert.equal(hostsAlive.size, 3) + assert.equal(hostsAlive.has('http://localhost:8080'), true) + assert.equal(hostsAlive.has('http://localhost:8081'), true) + assert.equal(hostsAlive.has('http://localhost:8082'), true) + }) + + it('three cores, one dead and round robin', async () => { + await startST() + await startST('localhost', 8082) + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080;http://localhost:8081/;http://localhost:8082', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const q = Querier.getNewInstanceOrThrowError(undefined) + assert.equal(await q.sendGetRequest(new NormalisedURLPath('/hello'), {}), 'Hello\n') + assert.equal(await q.sendPostRequest(new NormalisedURLPath('/hello'), {}), 'Hello\n') + let hostsAlive = q.getHostsAliveForTesting() + assert.equal(hostsAlive.size, 2) + assert.equal(await q.sendPutRequest(new NormalisedURLPath('/hello'), {}), 'Hello\n') // this will be the 4th API call + hostsAlive = q.getHostsAliveForTesting() + assert.equal(hostsAlive.size, 2) + assert.equal(hostsAlive.has('http://localhost:8080'), true) + assert.equal(hostsAlive.has('http://localhost:8081'), false) + assert.equal(hostsAlive.has('http://localhost:8082'), true) + }) + + it('test that no connectionURI given, but recipe used throws an error', async () => { + await startST() + ST.init({ + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + try { + await Session.getSessionInformation('') + assert(false) + } + catch (err) { + assert( + err.message + === 'No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using.', + ) + } + }) + + it('test that no connectionURI given, recipe override and used doesn\'t thrown an error', async () => { + await startST() + ST.init({ + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: (oI) => { + return { + ...oI, + getSessionInformation: async (input) => { + return input.sessionHandle + }, + } + }, + }, + }), + ], + }) + + assert((await Session.getSessionInformation('someHandle')) === 'someHandle') + }) + + it('test with core base path', async () => { + // first we need to know if the core used supports base_path config + await setKeyValueInConfig('base_path', '/test') + await startST() + + try { + await axios.get('http://localhost:8080/test/hello') + } + catch (error) { + if (error.response.status === 404) { + // core must be an older version, so we return early + return + } + throw error + } + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080/test', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + // we query the core now + const res = await Session.getAllSessionHandlesForUser('user1') + assert(res.length === 0) + }) + + it('test with incorrect core base path should fail', async () => { + // first we need to know if the core used supports base_path config + await setKeyValueInConfig('base_path', '/some/path') + await startST() + + try { + await axios.get('http://localhost:8080/some/path/hello') + } + catch (error) { + if (error.response.status === 404) { + // core must be an older version, so we return early + return + } + throw error + } + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080/test', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + try { + // we query the core now + await Session.getAllSessionHandlesForUser('user1') + fail() + } + catch (err) { + assert(err.message.startsWith('SuperTokens core threw an error')) + } + }) + + it('test with multiple core base path', async () => { + // first we need to know if the core used supports base_path config + await setKeyValueInConfig('base_path', '/some/path') + await startST() + + try { + await axios.get('http://localhost:8080/some/path/hello') + } + catch (error) { + if (error.response.status === 404) { + // core must be an older version, so we return early + return + } + throw error + } + + await setKeyValueInConfig('base_path', '/test') + await startST('localhost', 8082) + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080/some/path;http://localhost:8082/test', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + { + // we query the first core now + const res = await Session.getAllSessionHandlesForUser('user1') + assert(res.length === 0) + } + + { + // we query the second core now + const res = await Session.getAllSessionHandlesForUser('user1') + assert(res.length === 0) + } + }) +}) diff --git a/test/recipeModuleManager.test.js b/test/recipeModuleManager.test.js deleted file mode 100644 index 0f055c9c6..000000000 --- a/test/recipeModuleManager.test.js +++ /dev/null @@ -1,948 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, resetAll } = require("./utils"); -let { ProcessState } = require("../lib/build/processState"); -let ST = require("../"); -let Session = require("../recipe/session"); -let { Querier } = require("../lib/build/querier"); -let RecipeModule = require("../lib/build/recipeModule").default; -let NormalisedURLPath = require("../lib/build/normalisedURLPath").default; -let STError = require("../lib/build/error").default; -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -let EmailPassword = require("../recipe/emailpassword"); -let EmailPasswordRecipe = require("../lib/build/recipe/emailpassword/recipe").default; -const express = require("express"); -const assert = require("assert"); -const request = require("supertest"); -let { middleware, errorHandler } = require("../framework/express"); - -/** - * - * TODO: Create a recipe with two APIs that have the same path and method, and see it throw an error. - * TODO: (later) If a recipe has a callback and a user implements it, but throws a normal error from it, then we need to make sure that that error is caught only by their error handler - * TODO: (later) Make a custom validator throw an error and check that it's transformed into a general error, and then in user's error handler, it's a normal error again - * - */ - -describe(`recipeModuleManagerTest: ${printPath("[test/recipeModuleManager.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - resetTestRecipies(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("calling init multiple times", async function () { - await startST(); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailPassword.init(), - ], - }); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailPassword.init(), - ], - }); - }); - - // Check that querier has been inited when we call supertokens.init - // Failure condition: initalizing supertoknes before the the first try catch will fail the test - it("test that querier has been initiated when we call supertokens.init", async function () { - await startST(); - - try { - await Querier.getNewInstanceOrThrowError(undefined); - assert(false); - } catch (err) { - if (err.message !== "Please call the supertokens.init function before using SuperTokens") { - throw err; - } - } - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - await Querier.getNewInstanceOrThrowError(undefined); - }); - - // Check that modules have been inited when we call supertokens.init - // Failure condition: initalizing supertoknes before the the first try catch will fail the test - it("test that modules have been initiated when we call supertokens.init", async function () { - await startST(); - - try { - await SessionRecipe.getInstanceOrThrowError(); - assert(false); - } catch (err) { - if (err.message !== "Initialisation not done. Did you forget to call the SuperTokens.init function?") { - throw err; - } - } - - try { - await EmailPasswordRecipe.getInstanceOrThrowError(); - assert(false); - } catch (err) { - if (err.message !== "Initialisation not done. Did you forget to call the SuperTokens.init function?") { - throw err; - } - } - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailPassword.init(), - ], - }); - await SessionRecipe.getInstanceOrThrowError(); - await EmailPasswordRecipe.getInstanceOrThrowError(); - }); - - /* - Test various inputs to routing (if it accepts or not) - - including when the base path is "/" - - with and without a rId - - where we do not have to handle it and it skips it (with / without rId) - */ - - //Failure condition: Tests will fail is using the incorrect base path - it("test various inputs to routing with default base path", async function () { - await startST(); - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [TestRecipe.init()], - }); - const app = express(); - - app.use(middleware()); - - app.post("/auth/user-api", async (req, res) => { - res.status(200).json({ message: "success" }); - }); - - let r1 = await new Promise((resolve) => - request(app) - .post("/auth/") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success TestRecipe /"); - - r1 = await new Promise((resolve) => - request(app) - .post("/auth/") - .set("rid", "testRecipe") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success TestRecipe /"); - - r1 = await new Promise((resolve) => - request(app) - .post("/auth/user-api") - .set("rid", "testRecipe") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success"); - - r1 = await new Promise((resolve) => - request(app) - .post("/auth/user-api") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success"); - }); - - //Failure condition: Tests will fail is using the wrong base path - it("test various inputs to routing when base path is /", async function () { - await startST(); - { - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [TestRecipe.init()], - }); - const app = express(); - app.use(middleware()); - - app.post("/user-api", async (req, res) => { - res.status(200).json({ message: "success" }); - }); - - app.use(errorHandler()); - - let r1 = await new Promise((resolve) => - request(app) - .post("/") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success TestRecipe /"); - - r1 = await new Promise((resolve) => - request(app) - .post("/") - .set("rid", "testRecipe") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success TestRecipe /"); - - r1 = await new Promise((resolve) => - request(app) - .post("/user-api") - .set("rid", "testRecipe") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success"); - - r1 = await new Promise((resolve) => - request(app) - .post("/user-api") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success"); - - resetAll(); - } - }); - - //Failure condition: Tests will fail if the incorrect rid header value is set when sending a request the path - it("test routing with multiple recipes", async function () { - await startST(); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [TestRecipe.init(), TestRecipe1.init()], - }); - - const app = express(); - - app.use(middleware()); - - let r1 = await new Promise((resolve) => - request(app) - .post("/auth/hello") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(r1.body.message === "success TestRecipe /hello" || r1.body.message === "success TestRecipe1 /hello"); - - r1 = await new Promise((resolve) => - request(app) - .post("/auth/hello") - .set("rid", "testRecipe1") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(r1.body.message === "success TestRecipe1 /hello"); - - r1 = await new Promise((resolve) => - request(app) - .post("/auth/hello") - .set("rid", "testRecipe") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(r1.body.message === "success TestRecipe /hello"); - }); - - // Test various inputs to errorHandler (if it accepts or not) - it("test various inputs to errorHandler", async function () { - await startST(); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [TestRecipe.init(), TestRecipe1.init()], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - app.use((err, request, response, next) => { - if (err.message == "General error from TestRecipe") { - response.status(200).send("General error handled in user error handler"); - } else { - response.status(500).send("Invalid error"); - } - }); - - let r1 = await new Promise((resolve) => - request(app) - .post("/auth/error/general") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.text); - } - }) - ); - assert(r1 === "General error handled in user error handler"); - - r1 = await new Promise((resolve) => - request(app) - .post("/auth/error/badinput") - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(r1.status === 400); - assert(r1.body.message === "Bad input error from TestRecipe"); - }); - - // Error thrown from APIs implemented by recipes must not go unhandled - it("test that error thrown from APIs implemented by recipes must not go unhandled", async function () { - await startST(); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [TestRecipe.init()], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - app.use((err, req, res, next) => { - if (err.message === "error thrown in api") { - res.status(200).json({ message: "success" }); - } else { - res.status(200).json({ message: "failure" }); - } - }); - - let r1 = await new Promise((resolve) => - request(app) - .post("/auth/error") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "error from TestRecipe /error "); - - r1 = await new Promise((resolve) => - request(app) - .post("/auth/error/api-error") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert(r1.message === "success"); - }); - - // Disable a default route, and then implement your own API and check that that gets called - // Failure condition: in testRecipe1 if the disabled value for the /default-route-disabled is set to false, the test will fail - it("test if you diable a default route, and then implement your own API, your own api is called", async function () { - await startST(); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [TestRecipe1.init()], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/auth/default-route-disabled", async (req, res) => { - res.status(200).json({ message: "user defined api" }); - }); - - app.use(errorHandler()); - - let r1 = await new Promise((resolve) => - request(app) - .post("/auth/default-route-disabled") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body.message); - } - }) - ); - assert(r1 === "user defined api"); - }); - - // If an error handler in a recipe throws an error, that error next to go to the user's error handler - it("test if the error handler in a recipe throws an error, it goes to the user's error handler", async function () { - await startST(); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [TestRecipe.init()], - }); - - const app = express(); - - app.use(middleware()); - app.use(errorHandler()); - app.use((err, request, response, next) => { - if (err.message === "error from inside recipe error handler") { - response.status(200).send("user error handler"); - } else { - response.status(500).send("invalid error"); - } - }); - - let r1 = await new Promise((resolve) => - request(app) - .post("/auth/error/throw-error") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.text); - } - }) - ); - assert(r1 === "user error handler"); - }); - - // Test getAllCORSHeaders - it("test the getAllCORSHeaders function", async function () { - await startST(); - - ST.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [TestRecipe1.init(), TestRecipe2.init(), TestRecipe3.init(), TestRecipe3Duplicate.init()], - }); - let headers = await ST.getAllCORSHeaders(); - assert.strictEqual(headers.length, 5); - assert(headers.includes("rid")); - assert(headers.includes("fdi-version")); - assert(headers.includes("test-recipe-1")); - assert(headers.includes("test-recipe-2")); - assert(headers.includes("test-recipe-3")); - }); -}); - -class TestRecipe extends RecipeModule { - constructor(recipeId, appInfo) { - super(recipeId, appInfo); - } - - static init() { - return (appInfo) => { - if (TestRecipe.instance === undefined) { - TestRecipe.instance = new TestRecipe("testRecipe", appInfo); - return TestRecipe.instance; - } else { - throw new Error("already initialised"); - } - }; - } - - isErrorFromThisRecipe(err) { - return STError.isErrorFromSuperTokens(err) && err.fromRecipe === "testRecipe"; - } - - getAPIsHandled() { - return [ - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/"), - id: "/", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/hello"), - id: "/hello", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/error"), - id: "/error", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/error/api-error"), - id: "/error/api-error", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/error/general"), - id: "/error/general", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/error/badinput"), - id: "/error/badinput", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/error/throw-error"), - id: "/error/throw-error", - disabled: false, - }, - ]; - } - - async handleAPIRequest(id, req, res, next) { - if (id === "/") { - res.setStatusCode(200); - res.sendJSONResponse({ message: "success TestRecipe /" }); - return true; - } else if (id === "/hello") { - res.setStatusCode(200); - res.sendJSONResponse({ message: "success TestRecipe /hello" }); - return true; - } else if (id === "/error") { - throw new TestRecipeError({ - message: "error from TestRecipe /error ", - payload: undefined, - type: "ERROR_FROM_TEST_RECIPE", - }); - } else if (id === "/error/general") { - throw new Error("General error from TestRecipe"); - } else if (id === "/error/badinput") { - throw new TestRecipeError({ - message: "Bad input error from TestRecipe", - payload: undefined, - type: STError.BAD_INPUT_ERROR, - }); - } else if (id === "/error/throw-error") { - throw new TestRecipeError({ - message: "Error thrown from recipe error", - payload: undefined, - type: "ERROR_FROM_TEST_RECIPE_ERROR_HANDLER", - }); - } else if (id === "/error/api-error") { - throw new Error("error thrown in api"); - } - } - - handleError(err, request, response) { - if (err.type === "ERROR_FROM_TEST_RECIPE") { - response.setStatusCode(200); - response.sendJSONResponse({ message: err.message }); - } else if (err.type === "ERROR_FROM_TEST_RECIPE_ERROR_HANDLER") { - throw new Error("error from inside recipe error handler"); - } else { - throw err; - } - } - - getAllCORSHeaders() { - return []; - } - - static reset() { - this.instance = undefined; - } -} - -class TestRecipeError extends STError { - constructor(err) { - super(err); - this.fromRecipe = "testRecipe"; - } -} - -class TestRecipe1 extends RecipeModule { - constructor(recipeId, appInfo) { - super(recipeId, appInfo); - } - - static init() { - return (appInfo) => { - if (TestRecipe1.instance === undefined) { - TestRecipe1.instance = new TestRecipe1("testRecipe1", appInfo); - return TestRecipe1.instance; - } else { - throw new Error("already initialised"); - } - }; - } - - isErrorFromThisRecipe(err) { - return STError.isErrorFromSuperTokens(err) && err.fromRecipe === "testRecipe1"; - } - - getAPIsHandled() { - return [ - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/"), - id: "/", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/hello"), - id: "/hello", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/hello1"), - id: "/hello1", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/error"), - id: "/error", - disabled: false, - }, - { - method: "post", - pathWithoutApiBasePath: new NormalisedURLPath("/default-route-disabled"), - id: "/default-route-disabled", - disabled: true, - }, - ]; - } - - async handleAPIRequest(id, req, res) { - if (id === "/") { - res.setStatusCode(200); - res.sendJSONResponse({ message: "success TestRecipe1 /" }); - return true; - } else if (id === "/hello") { - res.setStatusCode(200); - res.sendJSONResponse({ message: "success TestRecipe1 /hello" }); - return true; - } else if (id === "/hello1") { - res.setStatusCode(200); - res.sendJSONResponse({ message: "success TestRecipe1 /hello1" }); - return true; - } else if (id === "/error") { - throw new TestRecipe1Error({ - message: "error from TestRecipe1 /error ", - payload: undefined, - type: "ERROR_FROM_TEST_RECIPE1", - }); - } else if (id === "/default-route-disabled") { - res.status(200); - res.sendJSONResponse({ message: "default route used" }); - return true; - } - } - - handleError(err, request, response) { - if (err.type === "ERROR_FROM_TEST_RECIPE1") { - response.setStatusCode(200); - res.sendJSONResponse({ message: err.message }); - } else { - throw err; - } - } - - getAllCORSHeaders() { - return ["test-recipe-1"]; - } - - static reset() { - this.instance = undefined; - } -} - -class TestRecipe1Error extends STError { - constructor(err) { - super(err); - this.fromRecipe = "testRecipe1"; - } -} - -class TestRecipe2 extends RecipeModule { - constructor(recipeId, appInfo) { - super(recipeId, appInfo); - } - - static init() { - return (appInfo) => { - if (TestRecipe2.instance === undefined) { - TestRecipe2.instance = new TestRecipe2("testRecipe2", appInfo); - return TestRecipe2.instance; - } else { - throw new Error("already initialised"); - } - }; - } - - getAPIsHandled() { - return []; - } - - getAllCORSHeaders() { - return ["test-recipe-2"]; - } - - static reset() { - this.instance = undefined; - } -} - -class TestRecipe3 extends RecipeModule { - constructor(recipeId, appInfo) { - super(recipeId, appInfo); - } - - static init() { - return (appInfo) => { - if (TestRecipe3.instance === undefined) { - TestRecipe3.instance = new TestRecipe3("testRecipe3", appInfo); - return TestRecipe3.instance; - } else { - throw new Error("already initialised"); - } - }; - } - - getAPIsHandled() { - return []; - } - - getAllCORSHeaders() { - return ["test-recipe-3"]; - } - - static reset() { - this.instance = undefined; - } -} - -class TestRecipe3Duplicate extends RecipeModule { - constructor(recipeId, appInfo) { - super(recipeId, appInfo); - } - - static init() { - return (appInfo) => { - if (TestRecipe3Duplicate.instance === undefined) { - TestRecipe3Duplicate.instance = new TestRecipe3("testRecipe3Duplicate", appInfo); - return TestRecipe3Duplicate.instance; - } else { - throw new Error("already initialised"); - } - }; - } - - getAPIsHandled() { - return []; - } - - getAllCORSHeaders() { - return ["test-recipe-3"]; - } - - static reset() { - this.instance = undefined; - } -} - -function resetTestRecipies() { - TestRecipe.reset(); - TestRecipe1.reset(); - TestRecipe2.reset(); - TestRecipe3.reset(); - TestRecipe3Duplicate.reset(); -} diff --git a/test/recipeModuleManager.test.ts b/test/recipeModuleManager.test.ts new file mode 100644 index 000000000..5b47b80a9 --- /dev/null +++ b/test/recipeModuleManager.test.ts @@ -0,0 +1,968 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import ST from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import RecipeModule from 'supertokens-node/recipeModule' +import NormalisedURLPath from 'supertokens-node/normalisedURLPath' +import STError from 'supertokens-node/error' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword/recipe' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, resetAll, setupST, startST } from './utils' + +/** + * + * TODO: Create a recipe with two APIs that have the same path and method, and see it throw an error. + * TODO: (later) If a recipe has a callback and a user implements it, but throws a normal error from it, then we need to make sure that that error is caught only by their error handler + * TODO: (later) Make a custom validator throw an error and check that it's transformed into a general error, and then in user's error handler, it's a normal error again + * + */ + +class TestRecipe extends RecipeModule { + constructor(recipeId, appInfo) { + super(recipeId, appInfo) + } + + static init() { + return (appInfo) => { + if (TestRecipe.instance === undefined) { + TestRecipe.instance = new TestRecipe('testRecipe', appInfo) + return TestRecipe.instance + } + else { + throw new Error('already initialised') + } + } + } + + isErrorFromThisRecipe(err) { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === 'testRecipe' + } + + getAPIsHandled() { + return [ + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/'), + id: '/', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/hello'), + id: '/hello', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/error'), + id: '/error', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/error/api-error'), + id: '/error/api-error', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/error/general'), + id: '/error/general', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/error/badinput'), + id: '/error/badinput', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/error/throw-error'), + id: '/error/throw-error', + disabled: false, + }, + ] + } + + async handleAPIRequest(id, req, res, next) { + if (id === '/') { + res.setStatusCode(200) + res.sendJSONResponse({ message: 'success TestRecipe /' }) + return true + } + else if (id === '/hello') { + res.setStatusCode(200) + res.sendJSONResponse({ message: 'success TestRecipe /hello' }) + return true + } + else if (id === '/error') { + throw new TestRecipeError({ + message: 'error from TestRecipe /error ', + payload: undefined, + type: 'ERROR_FROM_TEST_RECIPE', + }) + } + else if (id === '/error/general') { + throw new Error('General error from TestRecipe') + } + else if (id === '/error/badinput') { + throw new TestRecipeError({ + message: 'Bad input error from TestRecipe', + payload: undefined, + type: STError.BAD_INPUT_ERROR, + }) + } + else if (id === '/error/throw-error') { + throw new TestRecipeError({ + message: 'Error thrown from recipe error', + payload: undefined, + type: 'ERROR_FROM_TEST_RECIPE_ERROR_HANDLER', + }) + } + else if (id === '/error/api-error') { + throw new Error('error thrown in api') + } + } + + handleError(err, request, response) { + if (err.type === 'ERROR_FROM_TEST_RECIPE') { + response.setStatusCode(200) + response.sendJSONResponse({ message: err.message }) + } + else if (err.type === 'ERROR_FROM_TEST_RECIPE_ERROR_HANDLER') { + throw new Error('error from inside recipe error handler') + } + else { + throw err + } + } + + getAllCORSHeaders() { + return [] + } + + static reset() { + this.instance = undefined + } +} + +class TestRecipeError extends STError { + constructor(err) { + super(err) + this.fromRecipe = 'testRecipe' + } +} + +class TestRecipe1 extends RecipeModule { + constructor(recipeId, appInfo) { + super(recipeId, appInfo) + } + + static init() { + return (appInfo) => { + if (TestRecipe1.instance === undefined) { + TestRecipe1.instance = new TestRecipe1('testRecipe1', appInfo) + return TestRecipe1.instance + } + else { + throw new Error('already initialised') + } + } + } + + isErrorFromThisRecipe(err) { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === 'testRecipe1' + } + + getAPIsHandled() { + return [ + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/'), + id: '/', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/hello'), + id: '/hello', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/hello1'), + id: '/hello1', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/error'), + id: '/error', + disabled: false, + }, + { + method: 'post', + pathWithoutApiBasePath: new NormalisedURLPath('/default-route-disabled'), + id: '/default-route-disabled', + disabled: true, + }, + ] + } + + async handleAPIRequest(id, req, res) { + if (id === '/') { + res.setStatusCode(200) + res.sendJSONResponse({ message: 'success TestRecipe1 /' }) + return true + } + else if (id === '/hello') { + res.setStatusCode(200) + res.sendJSONResponse({ message: 'success TestRecipe1 /hello' }) + return true + } + else if (id === '/hello1') { + res.setStatusCode(200) + res.sendJSONResponse({ message: 'success TestRecipe1 /hello1' }) + return true + } + else if (id === '/error') { + throw new TestRecipe1Error({ + message: 'error from TestRecipe1 /error ', + payload: undefined, + type: 'ERROR_FROM_TEST_RECIPE1', + }) + } + else if (id === '/default-route-disabled') { + res.status(200) + res.sendJSONResponse({ message: 'default route used' }) + return true + } + } + + handleError(err, request, response) { + if (err.type === 'ERROR_FROM_TEST_RECIPE1') { + response.setStatusCode(200) + res.sendJSONResponse({ message: err.message }) + } + else { + throw err + } + } + + getAllCORSHeaders() { + return ['test-recipe-1'] + } + + static reset() { + this.instance = undefined + } +} + +class TestRecipe1Error extends STError { + constructor(err) { + super(err) + this.fromRecipe = 'testRecipe1' + } +} + +class TestRecipe2 extends RecipeModule { + constructor(recipeId, appInfo) { + super(recipeId, appInfo) + } + + static init() { + return (appInfo) => { + if (TestRecipe2.instance === undefined) { + TestRecipe2.instance = new TestRecipe2('testRecipe2', appInfo) + return TestRecipe2.instance + } + else { + throw new Error('already initialised') + } + } + } + + getAPIsHandled() { + return [] + } + + getAllCORSHeaders() { + return ['test-recipe-2'] + } + + static reset() { + this.instance = undefined + } +} + +class TestRecipe3 extends RecipeModule { + constructor(recipeId, appInfo) { + super(recipeId, appInfo) + } + + static init() { + return (appInfo) => { + if (TestRecipe3.instance === undefined) { + TestRecipe3.instance = new TestRecipe3('testRecipe3', appInfo) + return TestRecipe3.instance + } + else { + throw new Error('already initialised') + } + } + } + + getAPIsHandled() { + return [] + } + + getAllCORSHeaders() { + return ['test-recipe-3'] + } + + static reset() { + this.instance = undefined + } +} + +class TestRecipe3Duplicate extends RecipeModule { + constructor(recipeId, appInfo) { + super(recipeId, appInfo) + } + + static init() { + return (appInfo) => { + if (TestRecipe3Duplicate.instance === undefined) { + TestRecipe3Duplicate.instance = new TestRecipe3('testRecipe3Duplicate', appInfo) + return TestRecipe3Duplicate.instance + } + else { + throw new Error('already initialised') + } + } + } + + getAPIsHandled() { + return [] + } + + getAllCORSHeaders() { + return ['test-recipe-3'] + } + + static reset() { + this.instance = undefined + } +} + +function resetTestRecipies() { + TestRecipe.reset() + TestRecipe1.reset() + TestRecipe2.reset() + TestRecipe3.reset() + TestRecipe3Duplicate.reset() +} + +describe(`recipeModuleManagerTest: ${printPath('[test/recipeModuleManager.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + resetTestRecipies() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('calling init multiple times', async () => { + await startST() + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailPassword.init(), + ], + }) + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailPassword.init(), + ], + }) + }) + + // Check that querier has been inited when we call supertokens.init + // Failure condition: initalizing supertoknes before the the first try catch will fail the test + it('test that querier has been initiated when we call supertokens.init', async () => { + await startST() + + try { + await Querier.getNewInstanceOrThrowError(undefined) + assert(false) + } + catch (err) { + if (err.message !== 'Please call the supertokens.init function before using SuperTokens') + throw err + } + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + await Querier.getNewInstanceOrThrowError(undefined) + }) + + // Check that modules have been inited when we call supertokens.init + // Failure condition: initalizing supertoknes before the the first try catch will fail the test + it('test that modules have been initiated when we call supertokens.init', async () => { + await startST() + + try { + await SessionRecipe.getInstanceOrThrowError() + assert(false) + } + catch (err) { + if (err.message !== 'Initialisation not done. Did you forget to call the SuperTokens.init function?') + throw err + } + + try { + await EmailPasswordRecipe.getInstanceOrThrowError() + assert(false) + } + catch (err) { + if (err.message !== 'Initialisation not done. Did you forget to call the SuperTokens.init function?') + throw err + } + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailPassword.init(), + ], + }) + await SessionRecipe.getInstanceOrThrowError() + await EmailPasswordRecipe.getInstanceOrThrowError() + }) + + /* + Test various inputs to routing (if it accepts or not) + - including when the base path is "/" + - with and without a rId + - where we do not have to handle it and it skips it (with / without rId) + */ + + // Failure condition: Tests will fail is using the incorrect base path + it('test various inputs to routing with default base path', async () => { + await startST() + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [TestRecipe.init()], + }) + const app = express() + + app.use(middleware()) + + app.post('/auth/user-api', async (req, res) => { + res.status(200).json({ message: 'success' }) + }) + + let r1 = await new Promise(resolve => + request(app) + .post('/auth/') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success TestRecipe /') + + r1 = await new Promise(resolve => + request(app) + .post('/auth/') + .set('rid', 'testRecipe') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success TestRecipe /') + + r1 = await new Promise(resolve => + request(app) + .post('/auth/user-api') + .set('rid', 'testRecipe') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success') + + r1 = await new Promise(resolve => + request(app) + .post('/auth/user-api') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success') + }) + + // Failure condition: Tests will fail is using the wrong base path + it('test various inputs to routing when base path is /', async () => { + await startST() + { + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [TestRecipe.init()], + }) + const app = express() + app.use(middleware()) + + app.post('/user-api', async (req, res) => { + res.status(200).json({ message: 'success' }) + }) + + app.use(errorHandler()) + + let r1 = await new Promise(resolve => + request(app) + .post('/') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success TestRecipe /') + + r1 = await new Promise(resolve => + request(app) + .post('/') + .set('rid', 'testRecipe') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success TestRecipe /') + + r1 = await new Promise(resolve => + request(app) + .post('/user-api') + .set('rid', 'testRecipe') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success') + + r1 = await new Promise(resolve => + request(app) + .post('/user-api') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success') + + resetAll() + } + }) + + // Failure condition: Tests will fail if the incorrect rid header value is set when sending a request the path + it('test routing with multiple recipes', async () => { + await startST() + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [TestRecipe.init(), TestRecipe1.init()], + }) + + const app = express() + + app.use(middleware()) + + let r1 = await new Promise(resolve => + request(app) + .post('/auth/hello') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(r1.body.message === 'success TestRecipe /hello' || r1.body.message === 'success TestRecipe1 /hello') + + r1 = await new Promise(resolve => + request(app) + .post('/auth/hello') + .set('rid', 'testRecipe1') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(r1.body.message === 'success TestRecipe1 /hello') + + r1 = await new Promise(resolve => + request(app) + .post('/auth/hello') + .set('rid', 'testRecipe') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(r1.body.message === 'success TestRecipe /hello') + }) + + // Test various inputs to errorHandler (if it accepts or not) + it('test various inputs to errorHandler', async () => { + await startST() + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [TestRecipe.init(), TestRecipe1.init()], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + app.use((err, request, response, next) => { + if (err.message == 'General error from TestRecipe') + response.status(200).send('General error handled in user error handler') + + else + response.status(500).send('Invalid error') + }) + + let r1 = await new Promise(resolve => + request(app) + .post('/auth/error/general') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.text) + }), + ) + assert(r1 === 'General error handled in user error handler') + + r1 = await new Promise(resolve => + request(app) + .post('/auth/error/badinput') + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(r1.status === 400) + assert(r1.body.message === 'Bad input error from TestRecipe') + }) + + // Error thrown from APIs implemented by recipes must not go unhandled + it('test that error thrown from APIs implemented by recipes must not go unhandled', async () => { + await startST() + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [TestRecipe.init()], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + app.use((err, req, res, next) => { + if (err.message === 'error thrown in api') + res.status(200).json({ message: 'success' }) + + else + res.status(200).json({ message: 'failure' }) + }) + + let r1 = await new Promise(resolve => + request(app) + .post('/auth/error') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'error from TestRecipe /error ') + + r1 = await new Promise(resolve => + request(app) + .post('/auth/error/api-error') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert(r1.message === 'success') + }) + + // Disable a default route, and then implement your own API and check that that gets called + // Failure condition: in testRecipe1 if the disabled value for the /default-route-disabled is set to false, the test will fail + it('test if you diable a default route, and then implement your own API, your own api is called', async () => { + await startST() + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [TestRecipe1.init()], + }) + + const app = express() + + app.use(middleware()) + + app.post('/auth/default-route-disabled', async (req, res) => { + res.status(200).json({ message: 'user defined api' }) + }) + + app.use(errorHandler()) + + const r1 = await new Promise(resolve => + request(app) + .post('/auth/default-route-disabled') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body.message) + }), + ) + assert(r1 === 'user defined api') + }) + + // If an error handler in a recipe throws an error, that error next to go to the user's error handler + it('test if the error handler in a recipe throws an error, it goes to the user\'s error handler', async () => { + await startST() + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [TestRecipe.init()], + }) + + const app = express() + + app.use(middleware()) + app.use(errorHandler()) + app.use((err, request, response, next) => { + if (err.message === 'error from inside recipe error handler') + response.status(200).send('user error handler') + + else + response.status(500).send('invalid error') + }) + + const r1 = await new Promise(resolve => + request(app) + .post('/auth/error/throw-error') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.text) + }), + ) + assert(r1 === 'user error handler') + }) + + // Test getAllCORSHeaders + it('test the getAllCORSHeaders function', async () => { + await startST() + + ST.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [TestRecipe1.init(), TestRecipe2.init(), TestRecipe3.init(), TestRecipe3Duplicate.init()], + }) + const headers = await ST.getAllCORSHeaders() + assert.strictEqual(headers.length, 5) + assert(headers.includes('rid')) + assert(headers.includes('fdi-version')) + assert(headers.includes('test-recipe-1')) + assert(headers.includes('test-recipe-2')) + assert(headers.includes('test-recipe-3')) + }) +}) diff --git a/test/session.test.js b/test/session.test.js deleted file mode 100644 index dfb504be4..000000000 --- a/test/session.test.js +++ /dev/null @@ -1,1209 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, - killAllSTCoresOnly, - mockResponse, - mockRequest, -} = require("./utils"); -let assert = require("assert"); -let { Querier } = require("../lib/build/querier"); -const nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); -let SuperTokens = require("../"); -let Session = require("../recipe/session"); -let SessionFunctions = require("../lib/build/recipe/session/sessionFunctions"); -let { parseJWTWithoutSignatureVerification } = require("../lib/build/recipe/session/jwt"); -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -const { maxVersion } = require("../lib/build/utils"); -const { fail } = require("assert"); -let { middleware, errorHandler } = require("../framework/express"); - -/* TODO: -- the opposite of the above (check that if signing key changes, things are still fine) condition -- calling createNewSession twice, should overwrite the first call (in terms of cookies) -- calling createNewSession in the case of unauthorised error, should create a proper session -- revoking old session after create new session, should not remove new session's cookies. -- check that Access-Control-Expose-Headers header is being set properly during create, use and destroy session**** only for express -*/ - -describe(`session: ${printPath("[test/session.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // check if output headers and set cookies for create session is fine - it("test that output headers and set cookie for create session is fine", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res.header["access-control-expose-headers"] === "front-token, anti-csrf"); - - let cookies = extractInfoFromResponse(res); - assert(cookies.accessToken !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.antiCsrf !== undefined); - assert(cookies.accessTokenExpiry !== undefined); - assert(cookies.refreshTokenExpiry !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - assert(cookies.frontToken !== undefined); - }); - - // check if output headers and set cookies for refresh session is fine - it("test that output headers and set cookie for refresh session is fine", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res2.header["access-control-expose-headers"] === "front-token, anti-csrf"); - - let cookies = extractInfoFromResponse(res2); - assert(cookies.accessToken !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.antiCsrf !== undefined); - assert(cookies.accessTokenExpiry !== undefined); - assert(cookies.refreshTokenExpiry !== undefined); - assert(cookies.refreshToken !== undefined); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - assert(cookies.frontToken !== undefined); - }); - - // check if input cookies are missing, an appropriate error is thrown - // Failure condition: if valid cookies are set in the refresh call the test will fail - it("test that if input cookies are missing, an appropriate error is thrown", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res2.status === 401); - assert(JSON.parse(res2.text).message === "unauthorised"); - }); - - // check if input cookies are there, no error is thrown - // Failure condition: if cookies are no set in the refresh call the test will fail - it("test that if input cookies are there, no error is thrown", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res2.status === 200); - }); - - //- check for token theft detection - it("token theft detection", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let response = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - let response2 = await SessionFunctions.refreshSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - response.refreshToken.token, - response.antiCsrfToken, - true, - "cookie" - ); - - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response2.accessToken.token), - response2.antiCsrfToken, - true - ); - - try { - await SessionFunctions.refreshSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - response.refreshToken.token, - response.antiCsrfToken, - true, - "cookie", - "cookie" - ); - throw new Error("should not have come here"); - } catch (err) { - if (err.type !== Session.Error.TOKEN_THEFT_DETECTED) { - throw err; - } - } - }); - - it("token theft detection with API key", async function () { - await setKeyValueInConfig("api_keys", "shfo3h98308hOIHoei309saiho"); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - apiKey: "shfo3h98308hOIHoei309saiho", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let response = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - let response2 = await SessionFunctions.refreshSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - response.refreshToken.token, - response.antiCsrfToken, - true, - "cookie", - "cookie" - ); - - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response2.accessToken.token), - response2.antiCsrfToken, - true - ); - - try { - await SessionFunctions.refreshSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - response.refreshToken.token, - response.antiCsrfToken, - true, - "cookie", - "cookie" - ); - throw new Error("should not have come here"); - } catch (err) { - if (err.type !== Session.Error.TOKEN_THEFT_DETECTED) { - throw err; - } - } - }); - - it("query without API key", async function () { - await setKeyValueInConfig("api_keys", "shfo3h98308hOIHoei309saiho"); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - try { - await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - throw new Error("should not have come here"); - } catch (err) { - if ( - err.message !== - "SuperTokens core threw an error for a GET request to path: '/apiversion' with status code: 401 and message: Invalid API key\n" - ) { - throw err; - } - } - }); - - //check basic usage of session - it("test basic usage of sessions", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError(); - - let response = await SessionFunctions.createNewSession(s.recipeInterfaceImpl.helpers, "", false, {}, {}); - assert(response.session !== undefined); - assert(response.accessToken !== undefined); - assert(response.refreshToken !== undefined); - assert(response.antiCsrfToken !== undefined); - assert(Object.keys(response).length === 5); - - await SessionFunctions.getSession( - s.recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - response.antiCsrfToken, - true, - true - ); - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let response2 = await SessionFunctions.refreshSession( - s.recipeInterfaceImpl.helpers, - response.refreshToken.token, - response.antiCsrfToken, - true, - "cookie", - "cookie" - ); - assert(response2.session !== undefined); - assert(response2.accessToken !== undefined); - assert(response2.refreshToken !== undefined); - assert(response2.antiCsrfToken !== undefined); - assert(Object.keys(response2).length === 5); - - let response3 = await SessionFunctions.getSession( - s.recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response2.accessToken.token), - response2.antiCsrfToken, - true, - true - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(response3.session !== undefined); - assert(response3.accessToken !== undefined); - assert(Object.keys(response3).length === 2); - - ProcessState.getInstance().reset(); - - let response4 = await SessionFunctions.getSession( - s.recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response3.accessToken.token), - response2.antiCsrfToken, - true, - true - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - assert(response4.session !== undefined); - assert(response4.accessToken === undefined); - assert(Object.keys(response4).length === 1); - - let response5 = await SessionFunctions.revokeSession(s.recipeInterfaceImpl.helpers, response4.session.handle); - assert(response5 === true); - }); - - //check session verify for with / without anti-csrf present - it("test session verify with anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError(); - - let response = await SessionFunctions.createNewSession(s.recipeInterfaceImpl.helpers, "", false, {}, {}); - - let response2 = await SessionFunctions.getSession( - s.recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - response.antiCsrfToken, - true, - true - ); - assert(response2.session != undefined); - assert(Object.keys(response2.session).length === 3); - - let response3 = await SessionFunctions.getSession( - s.recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - response.antiCsrfToken, - false, - true - ); - assert(response3.session != undefined); - assert(Object.keys(response3.session).length === 3); - }); - - //check session verify for with / without anti-csrf present** - it("test session verify without anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError(); - - let response = await SessionFunctions.createNewSession(s.recipeInterfaceImpl.helpers, "", false, {}, {}); - - //passing anti-csrf token as undefined and anti-csrf check as false - let response2 = await SessionFunctions.getSession( - s.recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - undefined, - false, - true - ); - assert(response2.session != undefined); - assert(Object.keys(response2.session).length === 3); - - //passing anti-csrf token as undefined and anti-csrf check as true - try { - await SessionFunctions.getSession( - s.recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - undefined, - true, - true - ); - throw new Error("should not have come here"); - } catch (err) { - if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { - throw err; - } - } - }); - - //check revoking session(s) - it("test revoking of sessions", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //create a single session and revoke using the session handle - let res = await SessionFunctions.createNewSession(s.helpers, "someUniqueUserId", false, {}, {}); - let res2 = await SessionFunctions.revokeSession(s.helpers, res.session.handle); - assert(res2 === true); - - let res3 = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId"); - assert(res3.length === 0); - - //create multiple sessions with the same userID and use revokeAllSessionsForUser to revoke sessions - await SessionFunctions.createNewSession(s.helpers, "someUniqueUserId", false, {}, {}); - await SessionFunctions.createNewSession(s.helpers, "someUniqueUserId", false, {}, {}); - - let sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId"); - assert(sessionIdResponse.length === 2); - - let response = await SessionFunctions.revokeAllSessionsForUser(s.helpers, "someUniqueUserId"); - assert(response.length === 2); - - sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId"); - assert(sessionIdResponse.length === 0); - - //revoke a session with a session handle that does not exist - let resp = await SessionFunctions.revokeSession(s.helpers, ""); - assert(resp === false); - - //revoke a session with a userId that does not exist - let resp2 = await SessionFunctions.revokeAllSessionsForUser(s.helpers, "random"); - assert(resp2.length === 0); - }); - - //check manipulating session data - it("test manipulating session data", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "", false, {}, {}); - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value" }); - - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData; - assert.deepStrictEqual(res2, { key: "value" }); - - //changing the value of session data with the same key - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value 2" }); - - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData; - assert.deepStrictEqual(res3, { key: "value 2" }); - - //passing invalid session handle when updating session data - assert(!(await SessionFunctions.updateSessionData(s.helpers, "random", { key2: "value2" }))); - }); - - it("test manipulating session data with new get session function", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "", false, {}, {}); - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value" }); - - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res2.sessionData, { key: "value" }); - - //changing the value of session data with the same key - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value 2" }); - - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res3.sessionData, { key: "value 2" }); - - //passing invalid session handle when updating session data - assert(!(await SessionFunctions.updateSessionData(s.helpers, "random", { key2: "value2" }))); - }); - - it("test null and undefined values passed for session data", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "", false, {}, null); - - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData; - assert.deepStrictEqual(res2, {}); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value" }); - - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData; - assert.deepStrictEqual(res3, { key: "value" }); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, undefined); - - let res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData; - assert.deepStrictEqual(res4, {}); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value 2" }); - - let res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData; - assert.deepStrictEqual(res5, { key: "value 2" }); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, null); - - let res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData; - assert.deepStrictEqual(res6, {}); - }); - - it("test null and undefined values passed for session data with new get session method", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "", false, {}, null); - - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res2.sessionData, {}); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value" }); - - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res3.sessionData, { key: "value" }); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, undefined); - - let res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res4.sessionData, {}); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: "value 2" }); - - let res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res5.sessionData, { key: "value 2" }); - - await SessionFunctions.updateSessionData(s.helpers, res.session.handle, null); - - let res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res6.sessionData, {}); - }); - - //check manipulating jwt payload - it("test manipulating jwt payload", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "", false, {}, {}); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); - - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload; - assert.deepStrictEqual(res2, { key: "value" }); - - //changing the value of jwt payload with the same key - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); - - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload; - assert.deepStrictEqual(res3, { key: "value 2" }); - - //passing invalid session handle when updating jwt payload - assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, "random", { key2: "value2" }))); - }); - - it("test manipulating jwt payload with new get session method", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "", false, {}, {}); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); - - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res2.accessTokenPayload, { key: "value" }); - - //changing the value of jwt payload with the same key - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); - - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res3.accessTokenPayload, { key: "value 2" }); - - //passing invalid session handle when updating jwt payload - assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, "random", { key2: "value2" }))); - }); - - it("test null and undefined values passed for jwt payload", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "", false, null, {}); - - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload; - assert.deepStrictEqual(res2, {}); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); - - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload; - assert.deepStrictEqual(res3, { key: "value" }); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle); - - let res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, undefined)) - .accessTokenPayload; - assert.deepStrictEqual(res4, {}); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); - - let res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload; - assert.deepStrictEqual(res5, { key: "value 2" }); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null); - - let res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload; - assert.deepStrictEqual(res6, {}); - }); - - it("test null and undefined values passed for jwt payload with new get session method", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "", false, null, {}); - - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res2.accessTokenPayload, {}); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); - - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res3.accessTokenPayload, { key: "value" }); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle); - - let res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, undefined); - assert.deepStrictEqual(res4.accessTokenPayload, {}); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); - - let res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res5.accessTokenPayload, { key: "value 2" }); - - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null); - - let res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res6.accessTokenPayload, {}); - }); - - //if anti-csrf is disabled from ST core, check that not having that in input to verify session is fine** - it("test that when anti-csrf is disabled from ST core not having that in input to verify session is fine", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "NONE" })], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - let response = await SessionFunctions.createNewSession(s.helpers, "", false, {}, {}); - - //passing anti-csrf token as undefined and anti-csrf check as false - let response2 = await SessionFunctions.getSession( - s, - parseJWTWithoutSignatureVerification(response.accessToken.token), - undefined, - false, - true - ); - assert(response2.session != undefined); - assert(Object.keys(response2.session).length === 3); - - //passing anti-csrf token as undefined and anti-csrf check as true - let response3 = await SessionFunctions.getSession( - s, - parseJWTWithoutSignatureVerification(response.accessToken.token), - undefined, - true, - true - ); - assert(response3.session != undefined); - assert(Object.keys(response3.session).length === 3); - }); - - it("test that anti-csrf disabled and sameSite none does not throw an error", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "none", antiCsrf: "NONE" }), - ], - }); - }); - - it("test that anti-csrf disabled and sameSite lax does now throw an error", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "lax", antiCsrf: "NONE" }), - ], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - await SessionFunctions.createNewSession(s.helpers, "", false, {}, {}); - }); - - it("test that anti-csrf disabled and sameSite strict does now throw an error", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", cookieSameSite: "strict", antiCsrf: "NONE" }), - ], - }); - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - await SessionFunctions.createNewSession(s.helpers, "", false, {}, {}); - }); - - it("test that custom user id is returned correctly", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "customuserid", false, {}, null); - - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - - assert.strictEqual(res2.userId, "customuserid"); - }); - - it("test that get session by session handle payload is correct", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "", false, {}, null); - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - - assert(typeof res2.status === "string"); - assert(res2.status === "OK"); - assert(typeof res2.userId === "string"); - assert(typeof res2.sessionData === "object"); - assert(typeof res2.expiry === "number"); - assert(typeof res2.accessTokenPayload === "object"); - assert(typeof res2.timeCreated === "number"); - }); - - it("test that revoked session throws error when calling get session by session handle", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "someid", false, {}, null); - - let response = await SessionFunctions.revokeAllSessionsForUser(s.helpers, "someid"); - assert(response.length === 1); - - assert(!(await SessionFunctions.getSessionInformation(s.helpers, res.session.handle))); - }); - - it("should use override functions in sessioncontainer methods", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - getSessionInformation: async (input) => { - const info = await oI.getSessionInformation(input); - info.sessionData = { test: 1 }; - return info; - }, - }), - }, - }), - ], - }); - - const session = await Session.createNewSession(mockRequest(), mockResponse(), "testId"); - - const data = await session.getSessionData(); - - assert.equal(data.test, 1); - }); -}); diff --git a/test/session.test.ts b/test/session.test.ts new file mode 100644 index 000000000..23c16ed17 --- /dev/null +++ b/test/session.test.ts @@ -0,0 +1,1200 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { Querier } from 'supertokens-node/querier' +import express from 'express' +import request from 'supertest' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import * as SessionFunctions from 'supertokens-node/recipe/session/sessionFunctions' +import { parseJWTWithoutSignatureVerification } from 'supertokens-node/recipe/session/jwt' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { maxVersion } from 'supertokens-node/utils' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { + cleanST, + extractInfoFromResponse, + killAllST, + mockRequest, + mockResponse, + printPath, + setKeyValueInConfig, + setupST, + startST, +} from './utils' + +/* TODO: +- the opposite of the above (check that if signing key changes, things are still fine) condition +- calling createNewSession twice, should overwrite the first call (in terms of cookies) +- calling createNewSession in the case of unauthorised error, should create a proper session +- revoking old session after create new session, should not remove new session's cookies. +- check that Access-Control-Expose-Headers header is being set properly during create, use and destroy session**** only for express +*/ + +describe(`session: ${printPath('[test/session.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check if output headers and set cookies for create session is fine + it('test that output headers and set cookie for create session is fine', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(res.header['access-control-expose-headers'] === 'front-token, anti-csrf') + + const cookies = extractInfoFromResponse(res) + assert(cookies.accessToken !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.antiCsrf !== undefined) + assert(cookies.accessTokenExpiry !== undefined) + assert(cookies.refreshTokenExpiry !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + assert(cookies.frontToken !== undefined) + }) + + // check if output headers and set cookies for refresh session is fine + it('test that output headers and set cookie for refresh session is fine', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(res2.header['access-control-expose-headers'] === 'front-token, anti-csrf') + + const cookies = extractInfoFromResponse(res2) + assert(cookies.accessToken !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.antiCsrf !== undefined) + assert(cookies.accessTokenExpiry !== undefined) + assert(cookies.refreshTokenExpiry !== undefined) + assert(cookies.refreshToken !== undefined) + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + assert(cookies.frontToken !== undefined) + }) + + // check if input cookies are missing, an appropriate error is thrown + // Failure condition: if valid cookies are set in the refresh call the test will fail + it('test that if input cookies are missing, an appropriate error is thrown', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert(res2.status === 401) + assert(JSON.parse(res2.text).message === 'unauthorised') + }) + + // check if input cookies are there, no error is thrown + // Failure condition: if cookies are no set in the refresh call the test will fail + it('test that if input cookies are there, no error is thrown', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(res2.status === 200) + }) + + // - check for token theft detection + it('token theft detection', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const response = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + const response2 = await SessionFunctions.refreshSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + response.refreshToken.token, + response.antiCsrfToken, + true, + 'cookie', + ) + + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response2.accessToken.token), + response2.antiCsrfToken, + true, + ) + + try { + await SessionFunctions.refreshSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + response.refreshToken.token, + response.antiCsrfToken, + true, + 'cookie', + 'cookie', + ) + throw new Error('should not have come here') + } + catch (err) { + if (err.type !== Session.Error.TOKEN_THEFT_DETECTED) + throw err + } + }) + + it('token theft detection with API key', async () => { + await setKeyValueInConfig('api_keys', 'shfo3h98308hOIHoei309saiho') + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + apiKey: 'shfo3h98308hOIHoei309saiho', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const response = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + const response2 = await SessionFunctions.refreshSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + response.refreshToken.token, + response.antiCsrfToken, + true, + 'cookie', + 'cookie', + ) + + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response2.accessToken.token), + response2.antiCsrfToken, + true, + ) + + try { + await SessionFunctions.refreshSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + response.refreshToken.token, + response.antiCsrfToken, + true, + 'cookie', + 'cookie', + ) + throw new Error('should not have come here') + } + catch (err) { + if (err.type !== Session.Error.TOKEN_THEFT_DETECTED) + throw err + } + }) + + it('query without API key', async () => { + await setKeyValueInConfig('api_keys', 'shfo3h98308hOIHoei309saiho') + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + try { + await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + throw new Error('should not have come here') + } + catch (err) { + if ( + err.message + !== 'SuperTokens core threw an error for a GET request to path: \'/apiversion\' with status code: 401 and message: Invalid API key\n' + ) + throw err + } + }) + + // check basic usage of session + it('test basic usage of sessions', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError() + + const response = await SessionFunctions.createNewSession(s.recipeInterfaceImpl.helpers, '', false, {}, {}) + assert(response.session !== undefined) + assert(response.accessToken !== undefined) + assert(response.refreshToken !== undefined) + assert(response.antiCsrfToken !== undefined) + assert(Object.keys(response).length === 5) + + await SessionFunctions.getSession( + s.recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + response.antiCsrfToken, + true, + true, + ) + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const response2 = await SessionFunctions.refreshSession( + s.recipeInterfaceImpl.helpers, + response.refreshToken.token, + response.antiCsrfToken, + true, + 'cookie', + 'cookie', + ) + assert(response2.session !== undefined) + assert(response2.accessToken !== undefined) + assert(response2.refreshToken !== undefined) + assert(response2.antiCsrfToken !== undefined) + assert(Object.keys(response2).length === 5) + + const response3 = await SessionFunctions.getSession( + s.recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response2.accessToken.token), + response2.antiCsrfToken, + true, + true, + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(response3.session !== undefined) + assert(response3.accessToken !== undefined) + assert(Object.keys(response3).length === 2) + + ProcessState.getInstance().reset() + + const response4 = await SessionFunctions.getSession( + s.recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response3.accessToken.token), + response2.antiCsrfToken, + true, + true, + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + assert(response4.session !== undefined) + assert(response4.accessToken === undefined) + assert(Object.keys(response4).length === 1) + + const response5 = await SessionFunctions.revokeSession(s.recipeInterfaceImpl.helpers, response4.session.handle) + assert(response5 === true) + }) + + // check session verify for with / without anti-csrf present + it('test session verify with anti-csrf present', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError() + + const response = await SessionFunctions.createNewSession(s.recipeInterfaceImpl.helpers, '', false, {}, {}) + + const response2 = await SessionFunctions.getSession( + s.recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + response.antiCsrfToken, + true, + true, + ) + assert(response2.session != undefined) + assert(Object.keys(response2.session).length === 3) + + const response3 = await SessionFunctions.getSession( + s.recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + response.antiCsrfToken, + false, + true, + ) + assert(response3.session != undefined) + assert(Object.keys(response3.session).length === 3) + }) + + // check session verify for with / without anti-csrf present** + it('test session verify without anti-csrf present', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError() + + const response = await SessionFunctions.createNewSession(s.recipeInterfaceImpl.helpers, '', false, {}, {}) + + // passing anti-csrf token as undefined and anti-csrf check as false + const response2 = await SessionFunctions.getSession( + s.recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + undefined, + false, + true, + ) + assert(response2.session != undefined) + assert(Object.keys(response2.session).length === 3) + + // passing anti-csrf token as undefined and anti-csrf check as true + try { + await SessionFunctions.getSession( + s.recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + undefined, + true, + true, + ) + throw new Error('should not have come here') + } + catch (err) { + if (err.type !== Session.Error.TRY_REFRESH_TOKEN) + throw err + } + }) + + // check revoking session(s) + it('test revoking of sessions', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // create a single session and revoke using the session handle + const res = await SessionFunctions.createNewSession(s.helpers, 'someUniqueUserId', false, {}, {}) + const res2 = await SessionFunctions.revokeSession(s.helpers, res.session.handle) + assert(res2 === true) + + const res3 = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, 'someUniqueUserId') + assert(res3.length === 0) + + // create multiple sessions with the same userID and use revokeAllSessionsForUser to revoke sessions + await SessionFunctions.createNewSession(s.helpers, 'someUniqueUserId', false, {}, {}) + await SessionFunctions.createNewSession(s.helpers, 'someUniqueUserId', false, {}, {}) + + let sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, 'someUniqueUserId') + assert(sessionIdResponse.length === 2) + + const response = await SessionFunctions.revokeAllSessionsForUser(s.helpers, 'someUniqueUserId') + assert(response.length === 2) + + sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, 'someUniqueUserId') + assert(sessionIdResponse.length === 0) + + // revoke a session with a session handle that does not exist + const resp = await SessionFunctions.revokeSession(s.helpers, '') + assert(resp === false) + + // revoke a session with a userId that does not exist + const resp2 = await SessionFunctions.revokeAllSessionsForUser(s.helpers, 'random') + assert(resp2.length === 0) + }) + + // check manipulating session data + it('test manipulating session data', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding session data + const res = await SessionFunctions.createNewSession(s.helpers, '', false, {}, {}) + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value' }) + + const res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData + assert.deepStrictEqual(res2, { key: 'value' }) + + // changing the value of session data with the same key + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value 2' }) + + const res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData + assert.deepStrictEqual(res3, { key: 'value 2' }) + + // passing invalid session handle when updating session data + assert(!(await SessionFunctions.updateSessionData(s.helpers, 'random', { key2: 'value2' }))) + }) + + it('test manipulating session data with new get session function', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding session data + const res = await SessionFunctions.createNewSession(s.helpers, '', false, {}, {}) + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value' }) + + const res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res2.sessionData, { key: 'value' }) + + // changing the value of session data with the same key + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value 2' }) + + const res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res3.sessionData, { key: 'value 2' }) + + // passing invalid session handle when updating session data + assert(!(await SessionFunctions.updateSessionData(s.helpers, 'random', { key2: 'value2' }))) + }) + + it('test null and undefined values passed for session data', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding session data + const res = await SessionFunctions.createNewSession(s.helpers, '', false, {}, null) + + const res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData + assert.deepStrictEqual(res2, {}) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value' }) + + const res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData + assert.deepStrictEqual(res3, { key: 'value' }) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, undefined) + + const res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData + assert.deepStrictEqual(res4, {}) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value 2' }) + + const res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData + assert.deepStrictEqual(res5, { key: 'value 2' }) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, null) + + const res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionData + assert.deepStrictEqual(res6, {}) + }) + + it('test null and undefined values passed for session data with new get session method', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding session data + const res = await SessionFunctions.createNewSession(s.helpers, '', false, {}, null) + + const res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res2.sessionData, {}) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value' }) + + const res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res3.sessionData, { key: 'value' }) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, undefined) + + const res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res4.sessionData, {}) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, { key: 'value 2' }) + + const res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res5.sessionData, { key: 'value 2' }) + + await SessionFunctions.updateSessionData(s.helpers, res.session.handle, null) + + const res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res6.sessionData, {}) + }) + + // check manipulating jwt payload + it('test manipulating jwt payload', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding jwt payload + const res = await SessionFunctions.createNewSession(s.helpers, '', false, {}, {}) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value' }) + + const res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload + assert.deepStrictEqual(res2, { key: 'value' }) + + // changing the value of jwt payload with the same key + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value 2' }) + + const res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload + assert.deepStrictEqual(res3, { key: 'value 2' }) + + // passing invalid session handle when updating jwt payload + assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, 'random', { key2: 'value2' }))) + }) + + it('test manipulating jwt payload with new get session method', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding jwt payload + const res = await SessionFunctions.createNewSession(s.helpers, '', false, {}, {}) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value' }) + + const res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res2.accessTokenPayload, { key: 'value' }) + + // changing the value of jwt payload with the same key + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value 2' }) + + const res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res3.accessTokenPayload, { key: 'value 2' }) + + // passing invalid session handle when updating jwt payload + assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, 'random', { key2: 'value2' }))) + }) + + it('test null and undefined values passed for jwt payload', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding jwt payload + const res = await SessionFunctions.createNewSession(s.helpers, '', false, null, {}) + + const res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload + assert.deepStrictEqual(res2, {}) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value' }) + + const res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload + assert.deepStrictEqual(res3, { key: 'value' }) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle) + + const res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, undefined)) + .accessTokenPayload + assert.deepStrictEqual(res4, {}) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value 2' }) + + const res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload + assert.deepStrictEqual(res5, { key: 'value 2' }) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null) + + const res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).accessTokenPayload + assert.deepStrictEqual(res6, {}) + }) + + it('test null and undefined values passed for jwt payload with new get session method', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding jwt payload + const res = await SessionFunctions.createNewSession(s.helpers, '', false, null, {}) + + const res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res2.accessTokenPayload, {}) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value' }) + + const res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res3.accessTokenPayload, { key: 'value' }) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle) + + const res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, undefined) + assert.deepStrictEqual(res4.accessTokenPayload, {}) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: 'value 2' }) + + const res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res5.accessTokenPayload, { key: 'value 2' }) + + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null) + + const res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + assert.deepStrictEqual(res6.accessTokenPayload, {}) + }) + + // if anti-csrf is disabled from ST core, check that not having that in input to verify session is fine** + it('test that when anti-csrf is disabled from ST core not having that in input to verify session is fine', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'NONE' })], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + const response = await SessionFunctions.createNewSession(s.helpers, '', false, {}, {}) + + // passing anti-csrf token as undefined and anti-csrf check as false + const response2 = await SessionFunctions.getSession( + s, + parseJWTWithoutSignatureVerification(response.accessToken.token), + undefined, + false, + true, + ) + assert(response2.session != undefined) + assert(Object.keys(response2.session).length === 3) + + // passing anti-csrf token as undefined and anti-csrf check as true + const response3 = await SessionFunctions.getSession( + s, + parseJWTWithoutSignatureVerification(response.accessToken.token), + undefined, + true, + true, + ) + assert(response3.session != undefined) + assert(Object.keys(response3.session).length === 3) + }) + + it('test that anti-csrf disabled and sameSite none does not throw an error', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'none', antiCsrf: 'NONE' }), + ], + }) + }) + + it('test that anti-csrf disabled and sameSite lax does now throw an error', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'lax', antiCsrf: 'NONE' }), + ], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + await SessionFunctions.createNewSession(s.helpers, '', false, {}, {}) + }) + + it('test that anti-csrf disabled and sameSite strict does now throw an error', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', cookieSameSite: 'strict', antiCsrf: 'NONE' }), + ], + }) + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + await SessionFunctions.createNewSession(s.helpers, '', false, {}, {}) + }) + + it('test that custom user id is returned correctly', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding session data + const res = await SessionFunctions.createNewSession(s.helpers, 'customuserid', false, {}, null) + + const res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + + assert.strictEqual(res2.userId, 'customuserid') + }) + + it('test that get session by session handle payload is correct', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding session data + const res = await SessionFunctions.createNewSession(s.helpers, '', false, {}, null) + const res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle) + + assert(typeof res2.status === 'string') + assert(res2.status === 'OK') + assert(typeof res2.userId === 'string') + assert(typeof res2.sessionData === 'object') + assert(typeof res2.expiry === 'number') + assert(typeof res2.accessTokenPayload === 'object') + assert(typeof res2.timeCreated === 'number') + }) + + it('test that revoked session throws error when calling get session by session handle', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl + // adding session data + const res = await SessionFunctions.createNewSession(s.helpers, 'someid', false, {}, null) + + const response = await SessionFunctions.revokeAllSessionsForUser(s.helpers, 'someid') + assert(response.length === 1) + + assert(!(await SessionFunctions.getSessionInformation(s.helpers, res.session.handle))) + }) + + it('should use override functions in sessioncontainer methods', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + getSessionInformation: async (input) => { + const info = await oI.getSessionInformation(input) + info.sessionData = { test: 1 } + return info + }, + }), + }, + }), + ], + }) + + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'testId') + + const data = await session.getSessionData() + + assert.equal(data.test, 1) + }) +}) diff --git a/test/session/claims/assertClaims.test.js b/test/session/claims/assertClaims.test.js deleted file mode 100644 index 590bb7cc2..000000000 --- a/test/session/claims/assertClaims.test.js +++ /dev/null @@ -1,61 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath } = require("../../utils"); -const assert = require("assert"); -const { default: SessionClass } = require("../../../lib/build/recipe/session/sessionClass"); -const sinon = require("sinon"); -const { StubClaim } = require("./testClaims"); -const { default: getRecipeInterface } = require("../../../lib/build/recipe/session/recipeImplementation"); - -describe(`sessionClaims/assertClaims: ${printPath("[test/session/claims/assertClaims.test.js]")}`, function () { - describe("SessionClass.assertClaims", () => { - afterEach(() => { - sinon.restore(); - }); - it("should not throw for empty array", async () => { - const session = new SessionClass( - { getRecipeImpl: () => getRecipeInterface({}, {}) }, - "testToken", - "testHandle", - "testUserId", - {}, - {} - ); - const mock = sinon.mock(session).expects("updateAccessTokenPayload").never(); - - await session.assertClaims([]); - mock.verify(); - }); - - it("should call validate with the same payload object", async () => { - const payload = {}; - const session = new SessionClass( - { getRecipeImpl: () => getRecipeInterface({}, {}) }, - "testToken", - "testHandle", - "testUserId", - payload, - {} - ); - const mock = sinon.mock(session).expects("updateAccessTokenPayload").never(); - const claim = new StubClaim({ key: "st-c1", validateRes: { isValid: true } }); - - await session.assertClaims([claim.validators.stub]); - mock.verify(); - assert.equal(claim.validators.stub.validate.callCount, 1); - assert(claim.validators.stub.validate.calledWith(payload)); - }); - }); -}); diff --git a/test/session/claims/assertClaims.test.ts b/test/session/claims/assertClaims.test.ts new file mode 100644 index 000000000..b08fe3f6e --- /dev/null +++ b/test/session/claims/assertClaims.test.ts @@ -0,0 +1,63 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import SessionClass from 'supertokens-node/recipe/session/sessionClass' +import sinon from 'sinon' +import getRecipeInterface from 'supertokens-node/recipe/session/recipeImplementation' +import { afterEach, describe, it } from 'vitest' +import { printPath } from '../../utils' +import { StubClaim } from './testClaims' + +describe(`sessionClaims/assertClaims: ${printPath('[test/session/claims/assertClaims.test.ts]')}`, () => { + describe('SessionClass.assertClaims', () => { + afterEach(() => { + sinon.restore() + }) + it('should not throw for empty array', async () => { + const session = new SessionClass( + { getRecipeImpl: () => getRecipeInterface({}, {}) }, + 'testToken', + 'testHandle', + 'testUserId', + {}, + {}, + ) + const mock = sinon.mock(session).expects('updateAccessTokenPayload').never() + + await session.assertClaims([]) + mock.verify() + }) + + it('should call validate with the same payload object', async () => { + const payload = {} + const session = new SessionClass( + { getRecipeImpl: () => getRecipeInterface({}, {}) }, + 'testToken', + 'testHandle', + 'testUserId', + payload, + {}, + ) + const mock = sinon.mock(session).expects('updateAccessTokenPayload').never() + const claim = new StubClaim({ key: 'st-c1', validateRes: { isValid: true } }) + + await session.assertClaims([claim.validators.stub]) + mock.verify() + assert.equal(claim.validators.stub.validate.callCount, 1) + assert(claim.validators.stub.validate.calledWith(payload)) + }) + }) +}) diff --git a/test/session/claims/createNewSession.test.js b/test/session/claims/createNewSession.test.js deleted file mode 100644 index 18917cbd8..000000000 --- a/test/session/claims/createNewSession.test.js +++ /dev/null @@ -1,206 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, mockResponse, mockRequest } = require("../../utils"); -const assert = require("assert"); -const { ProcessState } = require("../../../lib/build/processState"); -const SuperTokens = require("../../.."); -const Session = require("../../../recipe/session"); -const { Querier } = require("../../../lib/build/querier"); -const { maxVersion } = require("../../../lib/build/utils"); -const { TrueClaim, UndefinedClaim } = require("./testClaims"); - -describe(`sessionClaims/createNewSession: ${printPath("[test/session/claims/createNewSession.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("createNewSession", () => { - it("should create access token payload w/ session claims", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.13 - if (maxVersion(apiVersion, "2.12") === "2.12") { - return; - } - const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "someId"); - - const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 1); - assert.ok(payload["st-true"]); - assert.equal(payload["st-true"].v, true); - assert(payload["st-true"].t > Date.now() - 1000); - }); - - it("should create access token payload wo/ session claims with an undefined value", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await UndefinedClaim.build( - input.userId, - input.accessTokenPayload, - input.userContext - )), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.13 - if (maxVersion(apiVersion, "2.12") === "2.12") { - return; - } - const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "someId"); - const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 0); - }); - - it("should merge claims and the passed access token payload obj", async function () { - await startST(); - const payloadParam = { initial: true }; - const custom2 = { undef: undefined, nullProp: null, inner: "asdf" }; - const customClaims = { - "user-custom": "asdf", - "user-custom2": custom2, - "user-custom3": null, - }; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - ...customClaims, - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.13 - if (maxVersion(apiVersion, "2.12") === "2.12") { - return; - } - const includesNullInPayload = maxVersion(apiVersion, "2.14") !== "2.14"; - const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "someId", payloadParam); - - // The passed object should be unchanged - assert.strictEqual(Object.keys(payloadParam).length, 1); - - const payload = res.getAccessTokenPayload(); - assert.strictEqual(Object.keys(payload).length, includesNullInPayload ? 5 : 4); - // We have the prop from the payload param - assert.strictEqual(payload["initial"], true); - // We have the boolean claim - assert.ok(payload["st-true"]); - assert.strictEqual(payload["st-true"].v, true); - assert(payload["st-true"].t > Date.now() - 1000); - // We have the custom claim - // The resulting payload is different from the input: it doesn't container undefined - assert.deepStrictEqual(payload["user-custom"], "asdf"); - if (includesNullInPayload) { - assert.deepStrictEqual(payload["user-custom2"], { - inner: "asdf", - nullProp: null, - }); - assert.deepStrictEqual(payload["user-custom3"], null); - } else { - assert.deepStrictEqual(payload["user-custom2"], { - inner: "asdf", - }); - assert.deepStrictEqual(payload["user-custom3"], undefined); - } - }); - }); -}); diff --git a/test/session/claims/createNewSession.test.ts b/test/session/claims/createNewSession.test.ts new file mode 100644 index 000000000..2a4138e87 --- /dev/null +++ b/test/session/claims/createNewSession.test.ts @@ -0,0 +1,209 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, mockRequest, mockResponse, printPath, setupST, startST } from '../../utils' +import { TrueClaim, UndefinedClaim } from './testClaims' + +describe(`sessionClaims/createNewSession: ${printPath('[test/session/claims/createNewSession.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('createNewSession', () => { + it('should create access token payload w/ session claims', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.13 + if (maxVersion(apiVersion, '2.12') === '2.12') + return + + const response = mockResponse() + const res = await Session.createNewSession(mockRequest(), response, 'someId') + + const payload = res.getAccessTokenPayload() + assert.equal(Object.keys(payload).length, 1) + assert.ok(payload['st-true']) + assert.equal(payload['st-true'].v, true) + assert(payload['st-true'].t > Date.now() - 1000) + }) + + it('should create access token payload wo/ session claims with an undefined value', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await UndefinedClaim.build( + input.userId, + input.accessTokenPayload, + input.userContext, + )), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.13 + if (maxVersion(apiVersion, '2.12') === '2.12') + return + + const response = mockResponse() + const res = await Session.createNewSession(mockRequest(), response, 'someId') + const payload = res.getAccessTokenPayload() + assert.equal(Object.keys(payload).length, 0) + }) + + it('should merge claims and the passed access token payload obj', async () => { + await startST() + const payloadParam = { initial: true } + const custom2 = { undef: undefined, nullProp: null, inner: 'asdf' } + const customClaims = { + 'user-custom': 'asdf', + 'user-custom2': custom2, + 'user-custom3': null, + } + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + ...customClaims, + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.13 + if (maxVersion(apiVersion, '2.12') === '2.12') + return + + const includesNullInPayload = maxVersion(apiVersion, '2.14') !== '2.14' + const response = mockResponse() + const res = await Session.createNewSession(mockRequest(), response, 'someId', payloadParam) + + // The passed object should be unchanged + assert.strictEqual(Object.keys(payloadParam).length, 1) + + const payload = res.getAccessTokenPayload() + assert.strictEqual(Object.keys(payload).length, includesNullInPayload ? 5 : 4) + // We have the prop from the payload param + assert.strictEqual(payload.initial, true) + // We have the boolean claim + assert.ok(payload['st-true']) + assert.strictEqual(payload['st-true'].v, true) + assert(payload['st-true'].t > Date.now() - 1000) + // We have the custom claim + // The resulting payload is different from the input: it doesn't container undefined + assert.deepStrictEqual(payload['user-custom'], 'asdf') + if (includesNullInPayload) { + assert.deepStrictEqual(payload['user-custom2'], { + inner: 'asdf', + nullProp: null, + }) + assert.deepStrictEqual(payload['user-custom3'], null) + } + else { + assert.deepStrictEqual(payload['user-custom2'], { + inner: 'asdf', + }) + assert.deepStrictEqual(payload['user-custom3'], undefined) + } + }) + }) +}) diff --git a/test/session/claims/fetchAndSetClaim.test.js b/test/session/claims/fetchAndSetClaim.test.js deleted file mode 100644 index cf4d0864f..000000000 --- a/test/session/claims/fetchAndSetClaim.test.js +++ /dev/null @@ -1,93 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, startST, killAllST, setupST, cleanST, mockResponse, mockRequest } = require("../../utils"); -const assert = require("assert"); -const { default: SessionClass } = require("../../../lib/build/recipe/session/sessionClass"); -const sinon = require("sinon"); -const { TrueClaim, UndefinedClaim } = require("./testClaims"); -const SuperTokens = require("../../.."); -const Session = require("../../../recipe/session"); -const { ProcessState } = require("../../../lib/build/processState"); - -describe(`sessionClaims/fetchAndSetClaim: ${printPath("[test/session/claims/fetchAndSetClaim.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("SessionClass.fetchAndSetClaim", () => { - afterEach(() => { - sinon.restore(); - }); - - it("should not change if claim fetchValue returns undefined", async () => { - const session = new SessionClass({}, "testToken", "testHandle", "testUserId", {}, {}); - - const mock = sinon.mock(session).expects("mergeIntoAccessTokenPayload").once().withArgs({}); - await session.fetchAndSetClaim(UndefinedClaim); - mock.verify(); - }); - - it("should update if claim fetchValue returns value", async () => { - const session = new SessionClass({}, "testToken", "testHandle", "testUserId", {}, {}); - sinon.useFakeTimers(); - const mock = sinon - .mock(session) - .expects("mergeIntoAccessTokenPayload") - .once() - .withArgs({ - "st-true": { - t: 0, - v: true, - }, - }); - await session.fetchAndSetClaim(TrueClaim); - mock.verify(); - }); - - it("should update using a handle if claim fetchValue returns a value", async () => { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "someId"); - - await Session.fetchAndSetClaim(res.getHandle(), TrueClaim); - - const payload = (await Session.getSessionInformation(res.getHandle())).accessTokenPayload; - assert.equal(Object.keys(payload).length, 1); - assert.ok(payload["st-true"]); - assert.equal(payload["st-true"].v, true); - assert(payload["st-true"].t > Date.now() - 1000); - }); - }); -}); diff --git a/test/session/claims/fetchAndSetClaim.test.ts b/test/session/claims/fetchAndSetClaim.test.ts new file mode 100644 index 000000000..11463c06b --- /dev/null +++ b/test/session/claims/fetchAndSetClaim.test.ts @@ -0,0 +1,95 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import SessionClass from 'supertokens-node/recipe/session/sessionClass' +import sinon from 'sinon' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, mockRequest, mockResponse, printPath, setupST, startST } from '../../utils' +import { TrueClaim, UndefinedClaim } from './testClaims' + +describe(`sessionClaims/fetchAndSetClaim: ${printPath('[test/session/claims/fetchAndSetClaim.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('SessionClass.fetchAndSetClaim', () => { + afterEach(() => { + sinon.restore() + }) + + it('should not change if claim fetchValue returns undefined', async () => { + const session = new SessionClass({}, 'testToken', 'testHandle', 'testUserId', {}, {}) + + const mock = sinon.mock(session).expects('mergeIntoAccessTokenPayload').once().withArgs({}) + await session.fetchAndSetClaim(UndefinedClaim) + mock.verify() + }) + + it('should update if claim fetchValue returns value', async () => { + const session = new SessionClass({}, 'testToken', 'testHandle', 'testUserId', {}, {}) + sinon.useFakeTimers() + const mock = sinon + .mock(session) + .expects('mergeIntoAccessTokenPayload') + .once() + .withArgs({ + 'st-true': { + t: 0, + v: true, + }, + }) + await session.fetchAndSetClaim(TrueClaim) + mock.verify() + }) + + it('should update using a handle if claim fetchValue returns a value', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const response = mockResponse() + const res = await Session.createNewSession(mockRequest(), response, 'someId') + + await Session.fetchAndSetClaim(res.getHandle(), TrueClaim) + + const payload = (await Session.getSessionInformation(res.getHandle())).accessTokenPayload + assert.equal(Object.keys(payload).length, 1) + assert.ok(payload['st-true']) + assert.equal(payload['st-true'].v, true) + assert(payload['st-true'].t > Date.now() - 1000) + }) + }) +}) diff --git a/test/session/claims/getClaimValue.test.js b/test/session/claims/getClaimValue.test.js deleted file mode 100644 index e88cfa419..000000000 --- a/test/session/claims/getClaimValue.test.js +++ /dev/null @@ -1,139 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, startST, killAllST, setupST, cleanST, mockResponse, mockRequest } = require("../../utils"); -const assert = require("assert"); -const SuperTokens = require("../../.."); -const Session = require("../../../recipe/session"); -const sinon = require("sinon"); -const { TrueClaim } = require("./testClaims"); -const { ProcessState } = require("../../../lib/build/processState"); - -describe(`sessionClaims/getClaimValue: ${printPath("[test/session/claims/getClaimValue.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("SessionClass.getClaimValue", () => { - afterEach(() => { - sinon.restore(); - }); - - it("should get the right value", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "someId"); - - const res = await session.getClaimValue(TrueClaim); - assert.equal(res, true); - }); - - it("should get the right value using session handle", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "someId"); - - const res = await Session.getClaimValue(session.getHandle(), TrueClaim); - assert.deepStrictEqual(res, { - status: "OK", - value: true, - }); - }); - - it("should work for not existing handle", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert.deepStrictEqual(await Session.getClaimValue("asfd", TrueClaim), { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }); - }); - }); -}); diff --git a/test/session/claims/getClaimValue.test.ts b/test/session/claims/getClaimValue.test.ts new file mode 100644 index 000000000..50743fb3f --- /dev/null +++ b/test/session/claims/getClaimValue.test.ts @@ -0,0 +1,141 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import sinon from 'sinon' +import { ProcessState } from 'supertokens-node/processState' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, mockRequest, mockResponse, printPath, setupST, startST } from '../../utils' +import { TrueClaim } from './testClaims' + +describe(`sessionClaims/getClaimValue: ${printPath('[test/session/claims/getClaimValue.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('SessionClass.getClaimValue', () => { + afterEach(() => { + sinon.restore() + }) + + it('should get the right value', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const response = mockResponse() + const session = await Session.createNewSession(mockRequest(), response, 'someId') + + const res = await session.getClaimValue(TrueClaim) + assert.equal(res, true) + }) + + it('should get the right value using session handle', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const response = mockResponse() + const session = await Session.createNewSession(mockRequest(), response, 'someId') + + const res = await Session.getClaimValue(session.getHandle(), TrueClaim) + assert.deepStrictEqual(res, { + status: 'OK', + value: true, + }) + }) + + it('should work for not existing handle', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert.deepStrictEqual(await Session.getClaimValue('asfd', TrueClaim), { + status: 'SESSION_DOES_NOT_EXIST_ERROR', + }) + }) + }) +}) diff --git a/test/session/claims/primitiveArrayClaim.test.js b/test/session/claims/primitiveArrayClaim.test.js deleted file mode 100644 index fa8613c10..000000000 --- a/test/session/claims/primitiveArrayClaim.test.js +++ /dev/null @@ -1,1010 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath } = require("../../utils"); -const assert = require("assert"); -const sinon = require("sinon"); -const { PrimitiveArrayClaim } = require("../../../lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim"); - -describe(`sessionClaims/primitiveArrayClaim: ${printPath( - "[test/session/claims/primitiveArrayClaim.test.js]" -)}`, function () { - describe("PrimitiveArrayClaim", () => { - afterEach(() => { - sinon.restore(); - }); - - describe("fetchAndSetClaim", () => { - it("should build the right payload", async () => { - const val = ["a"]; - const fetchValue = sinon.stub().returns(val); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - const ctx = {}; - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, { - asdf: { - t: now, - v: val, - }, - }); - }); - - it("should build the right payload w/ async fetch", async () => { - const val = ["a"]; - const fetchValue = sinon.stub().resolves(val); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - const ctx = {}; - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, { - asdf: { - t: now, - v: val, - }, - }); - }); - - it("should build the right payload matching addToPayload_internal", async () => { - const val = ["a"]; - const fetchValue = sinon.stub().resolves(val); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - const ctx = {}; - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, claim.addToPayload_internal({}, val)); - }); - - it("should call fetchValue with the right params", async () => { - const fetchValue = sinon.stub().returns(true); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - const userId = "userId"; - - const ctx = {}; - await claim.build(userId, ctx); - assert.strictEqual(fetchValue.callCount, 1); - assert.strictEqual(fetchValue.firstCall.args[0], userId); - assert.strictEqual(fetchValue.firstCall.args[1], ctx); - }); - - it("should leave payload empty if fetchValue returns undefined", async () => { - const fetchValue = sinon.stub().returns(undefined); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - - const ctx = {}; - - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, {}); - }); - }); - - describe("getValueFromPayload", () => { - it("should return undefined for empty payload", () => { - const val = ["a"]; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - - assert.strictEqual(claim.getValueFromPayload({}), undefined); - }); - - it("should return value set by fetchAndSetClaim", async () => { - const val = ["a"]; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - const payload = await claim.build("userId"); - - assert.strictEqual(claim.getValueFromPayload(payload), val); - }); - - it("should return value set by addToPayload_internal", async () => { - const val = ["a"]; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - const payload = await claim.addToPayload_internal({}, val); - assert.strictEqual(claim.getValueFromPayload(payload), val); - }); - }); - - describe("getLastRefetchTime", () => { - it("should return undefined for empty payload", () => { - const val = ["a"]; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - - assert.strictEqual(claim.getLastRefetchTime({}), undefined); - }); - - it("should return time matching the fetchAndSetClaim call", async () => { - const now = Date.now(); - sinon.useFakeTimers(now); - - const val = ["a"]; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue, - }); - const payload = await claim.build("userId"); - - assert.strictEqual(claim.getLastRefetchTime(payload), now); - }); - }); - - describe("validators.includes", () => { - const includedItem = "a"; - const notIncludedItem = "b"; - const val = [includedItem]; - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - }); - - const claimWithInfiniteMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, - }); - - const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: 600, - }); - - it("should not validate empty payload", async () => { - const res = await claimWithInfiniteMaxAge.validators.includes(includedItem, 600).validate({}, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToInclude: includedItem, - actualValue: undefined, - message: "value does not exist", - }, - }); - }); - - it("should not validate mismatching payload", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators - .includes(notIncludedItem, 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToInclude: notIncludedItem, - actualValue: val, - message: "wrong value", - }, - }); - }); - - it("should validate matching payload", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators.includes(includedItem, 600).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should not validate old values", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInfiniteMaxAge.validators.includes(includedItem, 600).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should validate old values if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInfiniteMaxAge.validators.includes(includedItem).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is not set", () => { - assert.equal(claimWithInfiniteMaxAge.validators.includes(notIncludedItem, 600).shouldRefetch({}), true); - }); - - it("should not refetch if value is set", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - - assert.equal( - claimWithInfiniteMaxAge.validators.includes(notIncludedItem, 600).shouldRefetch(payload), - false - ); - }); - - it("should refetch if value is old", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInfiniteMaxAge.validators.includes(notIncludedItem, 600).shouldRefetch(payload), - true - ); - }); - - it("should not refetch if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInfiniteMaxAge.validators.includes(notIncludedItem).shouldRefetch(payload), - false - ); - }); - - it("should not validate values older than defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithDefaultMaxAge.validators.includes(includedItem).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should validate old values with default defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claim.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claim.validators.includes(includedItem).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is older than defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal(claimWithDefaultMaxAge.validators.includes(notIncludedItem).shouldRefetch(payload), true); - }); - - it("should not refetch if maxAge is overrides to infinite", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithDefaultMaxAge.validators - .includes(notIncludedItem, Number.POSITIVE_INFINITY) - .shouldRefetch(payload), - false - ); - }); - }); - - describe("validators.excludes", () => { - const includedItem = "a"; - const notIncludedItem = "b"; - const val = [includedItem]; - - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - }); - - const claimWithInifiniteDefaultMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, - }); - - const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: 600, - }); - - it("should not validate empty payload", async () => { - const res = await claimWithInifiniteDefaultMaxAge.validators - .excludes(notIncludedItem, 600) - .validate({}, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToNotInclude: notIncludedItem, - actualValue: undefined, - message: "value does not exist", - }, - }); - }); - - it("should not validate mismatching payload", async () => { - const payload = await claimWithInifiniteDefaultMaxAge.build("userId"); - const res = await claimWithInifiniteDefaultMaxAge.validators - .excludes(includedItem, 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToNotInclude: includedItem, - actualValue: val, - message: "wrong value", - }, - }); - }); - - it("should validate matching payload", async () => { - const payload = await claimWithInifiniteDefaultMaxAge.build("userId"); - const res = await claimWithInifiniteDefaultMaxAge.validators - .excludes(notIncludedItem, 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should not validate old values", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInifiniteDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInifiniteDefaultMaxAge.validators - .excludes(notIncludedItem, 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should validate old values if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInifiniteDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInifiniteDefaultMaxAge.validators - .excludes(notIncludedItem) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is not set", () => { - assert.equal( - claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem, 600).shouldRefetch({}), - true - ); - }); - - it("should not refetch if value is set", async () => { - const payload = await claimWithInifiniteDefaultMaxAge.build("userId"); - - assert.equal( - claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem, 600).shouldRefetch(payload), - false - ); - }); - - it("should refetch if value is old", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInifiniteDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem, 600).shouldRefetch(payload), - true - ); - }); - - it("should not refetch if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInifiniteDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem).shouldRefetch(payload), - false - ); - }); - - it("should not validate values older than defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithDefaultMaxAge.validators.excludes(includedItem).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should validate old values with default defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claim.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claim.validators.excludes(notIncludedItem).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is older than defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal(claimWithDefaultMaxAge.validators.excludes(notIncludedItem).shouldRefetch(payload), true); - }); - - it("should not refetch if maxAge is overrides to infinite", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithDefaultMaxAge.validators - .excludes(notIncludedItem, Number.POSITIVE_INFINITY) - .shouldRefetch(payload), - false - ); - }); - }); - - describe("validators.includesAll", () => { - const includedItem = "a"; - const notIncludedItem = "b"; - const val = [includedItem]; - - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - }); - - const claimWithInfiniteMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, - }); - - const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: 600, - }); - - it("should not validate empty payload", async () => { - const res = await claimWithInfiniteMaxAge.validators.includesAll([includedItem], 600).validate({}, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToInclude: [includedItem], - actualValue: undefined, - message: "value does not exist", - }, - }); - }); - - it("should not validate mismatching payload", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators - .includesAll([includedItem, notIncludedItem], 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToInclude: [includedItem, notIncludedItem], - actualValue: val, - message: "wrong value", - }, - }); - }); - - it("should validate matching payload", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators - .includesAll([includedItem], 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should validate with requirement array", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators.includesAll([], 600).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should not validate old values", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInfiniteMaxAge.validators - .includesAll([includedItem], 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should validate old values if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInfiniteMaxAge.validators.includesAll([includedItem]).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is not set", () => { - assert.equal( - claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem], 600).shouldRefetch({}), - true - ); - }); - - it("should not refetch if value is set", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - - assert.equal( - claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem], 600).shouldRefetch(payload), - false - ); - }); - - it("should refetch if value is old", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem], 600).shouldRefetch(payload), - true - ); - }); - - it("should not refetch if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem]).shouldRefetch(payload), - false - ); - }); - - it("should not validate values older than defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithDefaultMaxAge.validators - .includesAll([notIncludedItem]) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should not refetch old values with default defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claim.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal(claim.validators.includesAll([notIncludedItem]).shouldRefetch(payload), false); - }); - - it("should not refetch if maxAge is overrides to infinite", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithDefaultMaxAge.validators - .includesAll([notIncludedItem], Number.POSITIVE_INFINITY) - .shouldRefetch(payload), - false - ); - }); - }); - - describe("validators.excludesAll", () => { - const includedItem = "a"; - const notIncludedItem = "b"; - const val = [includedItem]; - - const claim = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - }); - - const claimWithInfiniteMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, - }); - - const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: 600, - }); - - it("should not validate empty payload", async () => { - const res = await claimWithInfiniteMaxAge.validators - .excludesAll([notIncludedItem], 600) - .validate({}, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToNotInclude: [notIncludedItem], - actualValue: undefined, - message: "value does not exist", - }, - }); - }); - - it("should not validate mismatching payload", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators - .excludesAll([includedItem, notIncludedItem], 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedToNotInclude: [includedItem, notIncludedItem], - actualValue: val, - message: "wrong value", - }, - }); - }); - - it("should validate matching payload", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators - .excludesAll([notIncludedItem], 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should validate with empty array", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - const res = await claimWithInfiniteMaxAge.validators.excludesAll([], 600).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should not validate old values", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInfiniteMaxAge.validators - .excludesAll([notIncludedItem], 600) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should validate old values if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInfiniteMaxAge.validators - .excludesAll([notIncludedItem]) - .validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is not set", () => { - assert.equal( - claimWithInfiniteMaxAge.validators.excludesAll([includedItem], 600).shouldRefetch({}), - true - ); - }); - - it("should not refetch if value is set", async () => { - const payload = await claimWithInfiniteMaxAge.build("userId"); - - assert.equal( - claimWithInfiniteMaxAge.validators.excludesAll([includedItem], 600).shouldRefetch(payload), - false - ); - }); - - it("should refetch if value is old", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInfiniteMaxAge.validators.excludesAll([includedItem], 600).shouldRefetch(payload), - true - ); - }); - - it("should not refetch if maxAge is undefined", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInfiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithInfiniteMaxAge.validators.excludesAll([includedItem]).shouldRefetch(payload), - false - ); - }); - - it("should not validate values older than defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithDefaultMaxAge.validators.excludesAll([includedItem]).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should validate old values with default defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claim.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claim.validators.excludesAll([notIncludedItem]).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is older than defaultMaxAge", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithDefaultMaxAge.validators.excludesAll([includedItem]).shouldRefetch(payload), - true - ); - }); - - it("should not refetch if maxAge is overrides to infinite", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithDefaultMaxAge.validators - .excludesAll([includedItem], Number.POSITIVE_INFINITY) - .shouldRefetch(payload), - false - ); - }); - }); - }); -}); diff --git a/test/session/claims/primitiveArrayClaim.test.ts b/test/session/claims/primitiveArrayClaim.test.ts new file mode 100644 index 000000000..34056b0ab --- /dev/null +++ b/test/session/claims/primitiveArrayClaim.test.ts @@ -0,0 +1,1012 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import sinon from 'sinon' +import { PrimitiveArrayClaim } from 'supertokens-node/recipe/session/claimBaseClasses/primitiveArrayClaim' +import { afterEach, describe, it } from 'vitest' +import { printPath } from '../../utils' + +describe(`sessionClaims/primitiveArrayClaim: ${printPath( + '[test/session/claims/primitiveArrayClaim.test.ts]', +)}`, () => { + describe('PrimitiveArrayClaim', () => { + afterEach(() => { + sinon.restore() + }) + + describe('fetchAndSetClaim', () => { + it('should build the right payload', async () => { + const val = ['a'] + const fetchValue = sinon.stub().returns(val) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + const ctx = {} + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, { + asdf: { + t: now, + v: val, + }, + }) + }) + + it('should build the right payload w/ async fetch', async () => { + const val = ['a'] + const fetchValue = sinon.stub().resolves(val) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + const ctx = {} + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, { + asdf: { + t: now, + v: val, + }, + }) + }) + + it('should build the right payload matching addToPayload_internal', async () => { + const val = ['a'] + const fetchValue = sinon.stub().resolves(val) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + const ctx = {} + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, claim.addToPayload_internal({}, val)) + }) + + it('should call fetchValue with the right params', async () => { + const fetchValue = sinon.stub().returns(true) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + const userId = 'userId' + + const ctx = {} + await claim.build(userId, ctx) + assert.strictEqual(fetchValue.callCount, 1) + assert.strictEqual(fetchValue.firstCall.args[0], userId) + assert.strictEqual(fetchValue.firstCall.args[1], ctx) + }) + + it('should leave payload empty if fetchValue returns undefined', async () => { + const fetchValue = sinon.stub().returns(undefined) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + + const ctx = {} + + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, {}) + }) + }) + + describe('getValueFromPayload', () => { + it('should return undefined for empty payload', () => { + const val = ['a'] + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + + assert.strictEqual(claim.getValueFromPayload({}), undefined) + }) + + it('should return value set by fetchAndSetClaim', async () => { + const val = ['a'] + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + const payload = await claim.build('userId') + + assert.strictEqual(claim.getValueFromPayload(payload), val) + }) + + it('should return value set by addToPayload_internal', async () => { + const val = ['a'] + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + const payload = await claim.addToPayload_internal({}, val) + assert.strictEqual(claim.getValueFromPayload(payload), val) + }) + }) + + describe('getLastRefetchTime', () => { + it('should return undefined for empty payload', () => { + const val = ['a'] + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + + assert.strictEqual(claim.getLastRefetchTime({}), undefined) + }) + + it('should return time matching the fetchAndSetClaim call', async () => { + const now = Date.now() + sinon.useFakeTimers(now) + + const val = ['a'] + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue, + }) + const payload = await claim.build('userId') + + assert.strictEqual(claim.getLastRefetchTime(payload), now) + }) + }) + + describe('validators.includes', () => { + const includedItem = 'a' + const notIncludedItem = 'b' + const val = [includedItem] + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + }) + + const claimWithInfiniteMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, + }) + + const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: 600, + }) + + it('should not validate empty payload', async () => { + const res = await claimWithInfiniteMaxAge.validators.includes(includedItem, 600).validate({}, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToInclude: includedItem, + actualValue: undefined, + message: 'value does not exist', + }, + }) + }) + + it('should not validate mismatching payload', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators + .includes(notIncludedItem, 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToInclude: notIncludedItem, + actualValue: val, + message: 'wrong value', + }, + }) + }) + + it('should validate matching payload', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators.includes(includedItem, 600).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should not validate old values', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInfiniteMaxAge.validators.includes(includedItem, 600).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should validate old values if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInfiniteMaxAge.validators.includes(includedItem).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is not set', () => { + assert.equal(claimWithInfiniteMaxAge.validators.includes(notIncludedItem, 600).shouldRefetch({}), true) + }) + + it('should not refetch if value is set', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + + assert.equal( + claimWithInfiniteMaxAge.validators.includes(notIncludedItem, 600).shouldRefetch(payload), + false, + ) + }) + + it('should refetch if value is old', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInfiniteMaxAge.validators.includes(notIncludedItem, 600).shouldRefetch(payload), + true, + ) + }) + + it('should not refetch if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInfiniteMaxAge.validators.includes(notIncludedItem).shouldRefetch(payload), + false, + ) + }) + + it('should not validate values older than defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithDefaultMaxAge.validators.includes(includedItem).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should validate old values with default defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claim.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claim.validators.includes(includedItem).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is older than defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal(claimWithDefaultMaxAge.validators.includes(notIncludedItem).shouldRefetch(payload), true) + }) + + it('should not refetch if maxAge is overrides to infinite', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithDefaultMaxAge.validators + .includes(notIncludedItem, Number.POSITIVE_INFINITY) + .shouldRefetch(payload), + false, + ) + }) + }) + + describe('validators.excludes', () => { + const includedItem = 'a' + const notIncludedItem = 'b' + const val = [includedItem] + + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + }) + + const claimWithInifiniteDefaultMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, + }) + + const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: 600, + }) + + it('should not validate empty payload', async () => { + const res = await claimWithInifiniteDefaultMaxAge.validators + .excludes(notIncludedItem, 600) + .validate({}, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToNotInclude: notIncludedItem, + actualValue: undefined, + message: 'value does not exist', + }, + }) + }) + + it('should not validate mismatching payload', async () => { + const payload = await claimWithInifiniteDefaultMaxAge.build('userId') + const res = await claimWithInifiniteDefaultMaxAge.validators + .excludes(includedItem, 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToNotInclude: includedItem, + actualValue: val, + message: 'wrong value', + }, + }) + }) + + it('should validate matching payload', async () => { + const payload = await claimWithInifiniteDefaultMaxAge.build('userId') + const res = await claimWithInifiniteDefaultMaxAge.validators + .excludes(notIncludedItem, 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should not validate old values', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInifiniteDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInifiniteDefaultMaxAge.validators + .excludes(notIncludedItem, 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should validate old values if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInifiniteDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInifiniteDefaultMaxAge.validators + .excludes(notIncludedItem) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is not set', () => { + assert.equal( + claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem, 600).shouldRefetch({}), + true, + ) + }) + + it('should not refetch if value is set', async () => { + const payload = await claimWithInifiniteDefaultMaxAge.build('userId') + + assert.equal( + claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem, 600).shouldRefetch(payload), + false, + ) + }) + + it('should refetch if value is old', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInifiniteDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem, 600).shouldRefetch(payload), + true, + ) + }) + + it('should not refetch if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInifiniteDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInifiniteDefaultMaxAge.validators.excludes(includedItem).shouldRefetch(payload), + false, + ) + }) + + it('should not validate values older than defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithDefaultMaxAge.validators.excludes(includedItem).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should validate old values with default defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claim.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claim.validators.excludes(notIncludedItem).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is older than defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal(claimWithDefaultMaxAge.validators.excludes(notIncludedItem).shouldRefetch(payload), true) + }) + + it('should not refetch if maxAge is overrides to infinite', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithDefaultMaxAge.validators + .excludes(notIncludedItem, Number.POSITIVE_INFINITY) + .shouldRefetch(payload), + false, + ) + }) + }) + + describe('validators.includesAll', () => { + const includedItem = 'a' + const notIncludedItem = 'b' + const val = [includedItem] + + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + }) + + const claimWithInfiniteMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, + }) + + const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: 600, + }) + + it('should not validate empty payload', async () => { + const res = await claimWithInfiniteMaxAge.validators.includesAll([includedItem], 600).validate({}, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToInclude: [includedItem], + actualValue: undefined, + message: 'value does not exist', + }, + }) + }) + + it('should not validate mismatching payload', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators + .includesAll([includedItem, notIncludedItem], 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToInclude: [includedItem, notIncludedItem], + actualValue: val, + message: 'wrong value', + }, + }) + }) + + it('should validate matching payload', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators + .includesAll([includedItem], 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should validate with requirement array', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators.includesAll([], 600).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should not validate old values', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInfiniteMaxAge.validators + .includesAll([includedItem], 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should validate old values if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInfiniteMaxAge.validators.includesAll([includedItem]).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is not set', () => { + assert.equal( + claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem], 600).shouldRefetch({}), + true, + ) + }) + + it('should not refetch if value is set', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + + assert.equal( + claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem], 600).shouldRefetch(payload), + false, + ) + }) + + it('should refetch if value is old', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem], 600).shouldRefetch(payload), + true, + ) + }) + + it('should not refetch if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInfiniteMaxAge.validators.includesAll([notIncludedItem]).shouldRefetch(payload), + false, + ) + }) + + it('should not validate values older than defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithDefaultMaxAge.validators + .includesAll([notIncludedItem]) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should not refetch old values with default defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claim.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal(claim.validators.includesAll([notIncludedItem]).shouldRefetch(payload), false) + }) + + it('should not refetch if maxAge is overrides to infinite', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithDefaultMaxAge.validators + .includesAll([notIncludedItem], Number.POSITIVE_INFINITY) + .shouldRefetch(payload), + false, + ) + }) + }) + + describe('validators.excludesAll', () => { + const includedItem = 'a' + const notIncludedItem = 'b' + const val = [includedItem] + + const claim = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + }) + + const claimWithInfiniteMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, + }) + + const claimWithDefaultMaxAge = new PrimitiveArrayClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: 600, + }) + + it('should not validate empty payload', async () => { + const res = await claimWithInfiniteMaxAge.validators + .excludesAll([notIncludedItem], 600) + .validate({}, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToNotInclude: [notIncludedItem], + actualValue: undefined, + message: 'value does not exist', + }, + }) + }) + + it('should not validate mismatching payload', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators + .excludesAll([includedItem, notIncludedItem], 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedToNotInclude: [includedItem, notIncludedItem], + actualValue: val, + message: 'wrong value', + }, + }) + }) + + it('should validate matching payload', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators + .excludesAll([notIncludedItem], 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should validate with empty array', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + const res = await claimWithInfiniteMaxAge.validators.excludesAll([], 600).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should not validate old values', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInfiniteMaxAge.validators + .excludesAll([notIncludedItem], 600) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should validate old values if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInfiniteMaxAge.validators + .excludesAll([notIncludedItem]) + .validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is not set', () => { + assert.equal( + claimWithInfiniteMaxAge.validators.excludesAll([includedItem], 600).shouldRefetch({}), + true, + ) + }) + + it('should not refetch if value is set', async () => { + const payload = await claimWithInfiniteMaxAge.build('userId') + + assert.equal( + claimWithInfiniteMaxAge.validators.excludesAll([includedItem], 600).shouldRefetch(payload), + false, + ) + }) + + it('should refetch if value is old', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInfiniteMaxAge.validators.excludesAll([includedItem], 600).shouldRefetch(payload), + true, + ) + }) + + it('should not refetch if maxAge is undefined', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInfiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithInfiniteMaxAge.validators.excludesAll([includedItem]).shouldRefetch(payload), + false, + ) + }) + + it('should not validate values older than defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithDefaultMaxAge.validators.excludesAll([includedItem]).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should validate old values with default defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claim.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claim.validators.excludesAll([notIncludedItem]).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is older than defaultMaxAge', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithDefaultMaxAge.validators.excludesAll([includedItem]).shouldRefetch(payload), + true, + ) + }) + + it('should not refetch if maxAge is overrides to infinite', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithDefaultMaxAge.validators + .excludesAll([includedItem], Number.POSITIVE_INFINITY) + .shouldRefetch(payload), + false, + ) + }) + }) + }) +}) diff --git a/test/session/claims/primitiveClaim.test.js b/test/session/claims/primitiveClaim.test.js deleted file mode 100644 index 3ffd2571d..000000000 --- a/test/session/claims/primitiveClaim.test.js +++ /dev/null @@ -1,439 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath } = require("../../utils"); -const assert = require("assert"); -const sinon = require("sinon"); -const { PrimitiveClaim } = require("../../../recipe/session/claims"); - -describe(`sessionClaims/primitiveClaim: ${printPath("[test/session/claims/primitiveClaim.test.js]")}`, function () { - describe("PrimitiveClaim", () => { - afterEach(() => { - sinon.restore(); - }); - - describe("fetchAndSetClaim", () => { - it("should build the right payload", async () => { - const val = { a: 1 }; - const fetchValue = sinon.stub().returns(val); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - const ctx = {}; - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, { - asdf: { - t: now, - v: val, - }, - }); - }); - - it("should build the right payload w/ async fetch", async () => { - const val = { a: 1 }; - const fetchValue = sinon.stub().resolves(val); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - const ctx = {}; - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, { - asdf: { - t: now, - v: val, - }, - }); - }); - - it("should build the right payload matching addToPayload_internal", async () => { - const val = { a: 1 }; - const fetchValue = sinon.stub().resolves(val); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - const ctx = {}; - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, claim.addToPayload_internal({}, val)); - }); - - it("should call fetchValue with the right params", async () => { - const fetchValue = sinon.stub().returns(true); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - const userId = "userId"; - - const ctx = {}; - await claim.build(userId, ctx); - assert.strictEqual(fetchValue.callCount, 1); - assert.strictEqual(fetchValue.firstCall.args[0], userId); - assert.strictEqual(fetchValue.firstCall.args[1], ctx); - }); - - it("should leave payload empty if fetchValue returns undefined", async () => { - const fetchValue = sinon.stub().returns(undefined); - const now = Date.now(); - sinon.useFakeTimers(now); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - - const ctx = {}; - - const res = await claim.build("userId", ctx); - assert.deepStrictEqual(res, {}); - }); - }); - - describe("getValueFromPayload", () => { - it("should return undefined for empty payload", () => { - const val = { a: 1 }; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - - assert.strictEqual(claim.getValueFromPayload({}), undefined); - }); - - it("should return value set by fetchAndSetClaim", async () => { - const val = { a: 1 }; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - const payload = await claim.build("userId"); - - assert.strictEqual(claim.getValueFromPayload(payload), val); - }); - - it("should return value set by addToPayload_internal", async () => { - const val = { a: 1 }; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - const payload = await claim.addToPayload_internal({}, val); - assert.strictEqual(claim.getValueFromPayload(payload), val); - }); - }); - - describe("getLastRefetchTime", () => { - it("should return undefined for empty payload", () => { - const val = { a: 1 }; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - - assert.strictEqual(claim.getLastRefetchTime({}), undefined); - }); - - it("should return time matching the fetchAndSetClaim call", async () => { - const now = Date.now(); - sinon.useFakeTimers(now); - - const val = { a: 1 }; - const fetchValue = sinon.stub().resolves(val); - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue, - }); - const payload = await claim.build("userId"); - - assert.strictEqual(claim.getLastRefetchTime(payload), now); - }); - }); - - describe("validators.hasValue", () => { - const val = { a: 1 }; - const val2 = { b: 1 }; - const claim = new PrimitiveClaim({ - key: "asdf", - fetchValue: () => val, - }); - const claimWithInifiniteMaxAge = new PrimitiveClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, - }); - const claimWithDefaultMaxAge = new PrimitiveClaim({ - key: "asdf", - fetchValue: () => val, - defaultMaxAgeInSeconds: 600, - }); - - describe("with infinite defaultMaxAgeInSeconds", () => { - it("should not validate empty payload", async () => { - const res = await claimWithInifiniteMaxAge.validators.hasValue(val).validate({}, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedValue: val, - actualValue: undefined, - message: "value does not exist", - }, - }); - }); - - it("should not validate mismatching payload", async () => { - const payload = await claimWithInifiniteMaxAge.build("userId"); - const res = await claimWithInifiniteMaxAge.validators.hasValue(val2).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - expectedValue: val2, - actualValue: val, - message: "wrong value", - }, - }); - }); - - it("should validate matching payload", async () => { - const payload = await claimWithInifiniteMaxAge.build("userId"); - const res = await claimWithInifiniteMaxAge.validators.hasValue(val).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should validate old values as well", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInifiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInifiniteMaxAge.validators.hasValue(val).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is not set", async () => { - assert.equal(await claimWithInifiniteMaxAge.validators.hasValue(val2).shouldRefetch({}), true); - }); - - it("should not refetch if value is set", async () => { - const payload = await claimWithInifiniteMaxAge.build("userId"); - - assert.equal( - await claimWithInifiniteMaxAge.validators.hasValue(val2).shouldRefetch(payload), - false - ); - }); - }); - - describe("with set defaultMaxAgeInSeconds", () => { - it("should validate matching payload", async () => { - const payload = await claimWithDefaultMaxAge.build("userId"); - const res = await claimWithDefaultMaxAge.validators.hasValue(val).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should not validate old values", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithDefaultMaxAge.validators.hasValue(val).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should refetch if value is not set", () => { - assert.equal(claimWithDefaultMaxAge.validators.hasValue(val2).shouldRefetch({}), true); - }); - - it("should not refetch if value is set", async () => { - const payload = await claimWithDefaultMaxAge.build("userId"); - - assert.equal(claimWithDefaultMaxAge.validators.hasValue(val2).shouldRefetch(payload), false); - }); - - it("should refetch if value is old", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal(claimWithDefaultMaxAge.validators.hasValue(val2).shouldRefetch(payload), true); - }); - - it("should not refetch with an overridden maxAgeInSeconds", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithDefaultMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claimWithDefaultMaxAge.validators - .hasValue(val2, Number.POSITIVE_INFINITY) - .shouldRefetch(payload), - false - ); - }); - }); - - describe("with default defaultMaxAgeInSeconds", () => { - it("should validate matching payload", async () => { - const payload = await claim.build("userId"); - const res = await claim.validators.hasValue(val).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should validate old values", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claim.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claim.validators.hasValue(val).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should refetch if value is not set", () => { - assert.equal(claim.validators.hasValue(val2).shouldRefetch({}), true); - }); - - it("should not refetch if value is set", async () => { - const payload = await claim.build("userId"); - - assert.equal(claim.validators.hasValue(val2).shouldRefetch(payload), false); - }); - - it("should refetch if value is old", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claim.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal(claim.validators.hasValue(val2).shouldRefetch(payload), false); - }); - - it("should not refetch with an overridden maxAgeInSeconds", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claim.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal( - claim.validators.hasValue(val2, Number.POSITIVE_INFINITY).shouldRefetch(payload), - false - ); - }); - }); - - describe("with maxAgeInSeconds", () => { - it("should validate matching payload", async () => { - const payload = await claimWithInifiniteMaxAge.build("userId"); - const res = await claimWithInifiniteMaxAge.validators.hasValue(val, 600).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: true, - }); - }); - - it("should not validate old values", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInifiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - const res = await claimWithInifiniteMaxAge.validators.hasValue(val, 600).validate(payload, {}); - assert.deepStrictEqual(res, { - isValid: false, - reason: { - ageInSeconds: 604800, - maxAgeInSeconds: 600, - message: "expired", - }, - }); - }); - - it("should refetch if value is not set", () => { - assert.equal(claimWithInifiniteMaxAge.validators.hasValue(val2, 600).shouldRefetch({}), true); - }); - - it("should not refetch if value is set", async () => { - const payload = await claimWithInifiniteMaxAge.build("userId"); - - assert.equal(claimWithInifiniteMaxAge.validators.hasValue(val2, 600).shouldRefetch(payload), false); - }); - - it("should refetch if value is old", async () => { - const now = Date.now(); - const clock = sinon.useFakeTimers(now); - - const payload = await claimWithInifiniteMaxAge.build("userId"); - - // advance clock by one week - clock.tick(6.048e8); - - assert.equal(claimWithInifiniteMaxAge.validators.hasValue(val2, 600).shouldRefetch(payload), true); - }); - }); - }); - }); -}); diff --git a/test/session/claims/primitiveClaim.test.ts b/test/session/claims/primitiveClaim.test.ts new file mode 100644 index 000000000..4764db9a6 --- /dev/null +++ b/test/session/claims/primitiveClaim.test.ts @@ -0,0 +1,441 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import sinon from 'sinon' +import { PrimitiveClaim } from 'supertokens-node/recipe/session/claims' +import { afterEach, describe, it } from 'vitest' +import { printPath } from '../../utils' + +describe(`sessionClaims/primitiveClaim: ${printPath('[test/session/claims/primitiveClaim.test.ts]')}`, () => { + describe('PrimitiveClaim', () => { + afterEach(() => { + sinon.restore() + }) + + describe('fetchAndSetClaim', () => { + it('should build the right payload', async () => { + const val = { a: 1 } + const fetchValue = sinon.stub().returns(val) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + const ctx = {} + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, { + asdf: { + t: now, + v: val, + }, + }) + }) + + it('should build the right payload w/ async fetch', async () => { + const val = { a: 1 } + const fetchValue = sinon.stub().resolves(val) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + const ctx = {} + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, { + asdf: { + t: now, + v: val, + }, + }) + }) + + it('should build the right payload matching addToPayload_internal', async () => { + const val = { a: 1 } + const fetchValue = sinon.stub().resolves(val) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + const ctx = {} + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, claim.addToPayload_internal({}, val)) + }) + + it('should call fetchValue with the right params', async () => { + const fetchValue = sinon.stub().returns(true) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + const userId = 'userId' + + const ctx = {} + await claim.build(userId, ctx) + assert.strictEqual(fetchValue.callCount, 1) + assert.strictEqual(fetchValue.firstCall.args[0], userId) + assert.strictEqual(fetchValue.firstCall.args[1], ctx) + }) + + it('should leave payload empty if fetchValue returns undefined', async () => { + const fetchValue = sinon.stub().returns(undefined) + const now = Date.now() + sinon.useFakeTimers(now) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + + const ctx = {} + + const res = await claim.build('userId', ctx) + assert.deepStrictEqual(res, {}) + }) + }) + + describe('getValueFromPayload', () => { + it('should return undefined for empty payload', () => { + const val = { a: 1 } + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + + assert.strictEqual(claim.getValueFromPayload({}), undefined) + }) + + it('should return value set by fetchAndSetClaim', async () => { + const val = { a: 1 } + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + const payload = await claim.build('userId') + + assert.strictEqual(claim.getValueFromPayload(payload), val) + }) + + it('should return value set by addToPayload_internal', async () => { + const val = { a: 1 } + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + const payload = await claim.addToPayload_internal({}, val) + assert.strictEqual(claim.getValueFromPayload(payload), val) + }) + }) + + describe('getLastRefetchTime', () => { + it('should return undefined for empty payload', () => { + const val = { a: 1 } + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + + assert.strictEqual(claim.getLastRefetchTime({}), undefined) + }) + + it('should return time matching the fetchAndSetClaim call', async () => { + const now = Date.now() + sinon.useFakeTimers(now) + + const val = { a: 1 } + const fetchValue = sinon.stub().resolves(val) + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue, + }) + const payload = await claim.build('userId') + + assert.strictEqual(claim.getLastRefetchTime(payload), now) + }) + }) + + describe('validators.hasValue', () => { + const val = { a: 1 } + const val2 = { b: 1 } + const claim = new PrimitiveClaim({ + key: 'asdf', + fetchValue: () => val, + }) + const claimWithInifiniteMaxAge = new PrimitiveClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: Number.POSITIVE_INFINITY, + }) + const claimWithDefaultMaxAge = new PrimitiveClaim({ + key: 'asdf', + fetchValue: () => val, + defaultMaxAgeInSeconds: 600, + }) + + describe('with infinite defaultMaxAgeInSeconds', () => { + it('should not validate empty payload', async () => { + const res = await claimWithInifiniteMaxAge.validators.hasValue(val).validate({}, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedValue: val, + actualValue: undefined, + message: 'value does not exist', + }, + }) + }) + + it('should not validate mismatching payload', async () => { + const payload = await claimWithInifiniteMaxAge.build('userId') + const res = await claimWithInifiniteMaxAge.validators.hasValue(val2).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + expectedValue: val2, + actualValue: val, + message: 'wrong value', + }, + }) + }) + + it('should validate matching payload', async () => { + const payload = await claimWithInifiniteMaxAge.build('userId') + const res = await claimWithInifiniteMaxAge.validators.hasValue(val).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should validate old values as well', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInifiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInifiniteMaxAge.validators.hasValue(val).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is not set', async () => { + assert.equal(await claimWithInifiniteMaxAge.validators.hasValue(val2).shouldRefetch({}), true) + }) + + it('should not refetch if value is set', async () => { + const payload = await claimWithInifiniteMaxAge.build('userId') + + assert.equal( + await claimWithInifiniteMaxAge.validators.hasValue(val2).shouldRefetch(payload), + false, + ) + }) + }) + + describe('with set defaultMaxAgeInSeconds', () => { + it('should validate matching payload', async () => { + const payload = await claimWithDefaultMaxAge.build('userId') + const res = await claimWithDefaultMaxAge.validators.hasValue(val).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should not validate old values', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithDefaultMaxAge.validators.hasValue(val).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should refetch if value is not set', () => { + assert.equal(claimWithDefaultMaxAge.validators.hasValue(val2).shouldRefetch({}), true) + }) + + it('should not refetch if value is set', async () => { + const payload = await claimWithDefaultMaxAge.build('userId') + + assert.equal(claimWithDefaultMaxAge.validators.hasValue(val2).shouldRefetch(payload), false) + }) + + it('should refetch if value is old', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal(claimWithDefaultMaxAge.validators.hasValue(val2).shouldRefetch(payload), true) + }) + + it('should not refetch with an overridden maxAgeInSeconds', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithDefaultMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claimWithDefaultMaxAge.validators + .hasValue(val2, Number.POSITIVE_INFINITY) + .shouldRefetch(payload), + false, + ) + }) + }) + + describe('with default defaultMaxAgeInSeconds', () => { + it('should validate matching payload', async () => { + const payload = await claim.build('userId') + const res = await claim.validators.hasValue(val).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should validate old values', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claim.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claim.validators.hasValue(val).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should refetch if value is not set', () => { + assert.equal(claim.validators.hasValue(val2).shouldRefetch({}), true) + }) + + it('should not refetch if value is set', async () => { + const payload = await claim.build('userId') + + assert.equal(claim.validators.hasValue(val2).shouldRefetch(payload), false) + }) + + it('should refetch if value is old', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claim.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal(claim.validators.hasValue(val2).shouldRefetch(payload), false) + }) + + it('should not refetch with an overridden maxAgeInSeconds', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claim.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal( + claim.validators.hasValue(val2, Number.POSITIVE_INFINITY).shouldRefetch(payload), + false, + ) + }) + }) + + describe('with maxAgeInSeconds', () => { + it('should validate matching payload', async () => { + const payload = await claimWithInifiniteMaxAge.build('userId') + const res = await claimWithInifiniteMaxAge.validators.hasValue(val, 600).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: true, + }) + }) + + it('should not validate old values', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInifiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + const res = await claimWithInifiniteMaxAge.validators.hasValue(val, 600).validate(payload, {}) + assert.deepStrictEqual(res, { + isValid: false, + reason: { + ageInSeconds: 604800, + maxAgeInSeconds: 600, + message: 'expired', + }, + }) + }) + + it('should refetch if value is not set', () => { + assert.equal(claimWithInifiniteMaxAge.validators.hasValue(val2, 600).shouldRefetch({}), true) + }) + + it('should not refetch if value is set', async () => { + const payload = await claimWithInifiniteMaxAge.build('userId') + + assert.equal(claimWithInifiniteMaxAge.validators.hasValue(val2, 600).shouldRefetch(payload), false) + }) + + it('should refetch if value is old', async () => { + const now = Date.now() + const clock = sinon.useFakeTimers(now) + + const payload = await claimWithInifiniteMaxAge.build('userId') + + // advance clock by one week + clock.tick(6.048e8) + + assert.equal(claimWithInifiniteMaxAge.validators.hasValue(val2, 600).shouldRefetch(payload), true) + }) + }) + }) + }) +}) diff --git a/test/session/claims/removeClaim.test.js b/test/session/claims/removeClaim.test.js deleted file mode 100644 index 9dd9e68b6..000000000 --- a/test/session/claims/removeClaim.test.js +++ /dev/null @@ -1,179 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, startST, killAllST, setupST, cleanST, mockResponse, mockRequest } = require("../../utils"); -const assert = require("assert"); -const SuperTokens = require("../../.."); -const Session = require("../../../recipe/session"); -const { default: SessionClass } = require("../../../lib/build/recipe/session/sessionClass"); -const sinon = require("sinon"); -const { TrueClaim } = require("./testClaims"); -const { ProcessState } = require("../../../lib/build/processState"); - -describe(`sessionClaims/removeClaim: ${printPath("[test/session/claims/removeClaim.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("SessionClass.removeClaim", () => { - afterEach(() => { - sinon.restore(); - }); - - it("should attempt to set claim to null", async () => { - const session = new SessionClass({}, "testToken", "testHandle", "testUserId", {}, {}); - sinon.useFakeTimers(); - const mock = sinon.mock(session).expects("mergeIntoAccessTokenPayload").once().withArgs({ - "st-true": null, - }); - await session.removeClaim(TrueClaim); - mock.verify(); - }); - - it("should clear previously set claim", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "someId"); - - const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 1); - assert.ok(payload["st-true"]); - assert.equal(payload["st-true"].v, true); - assert(payload["st-true"].t > Date.now() - 10000); - - await res.removeClaim(TrueClaim); - - const payloadAfter = res.getAccessTokenPayload(); - assert.equal(Object.keys(payloadAfter).length, 0); - }); - - it("should clear previously set claim using a handle", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "someId"); - - const payload = session.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 1); - assert.ok(payload["st-true"]); - assert.equal(payload["st-true"].v, true); - assert(payload["st-true"].t > Date.now() - 10000); - - const res = await Session.removeClaim(session.getHandle(), TrueClaim); - assert.equal(res, true); - - const payloadAfter = (await Session.getSessionInformation(session.getHandle())).accessTokenPayload; - assert.equal(Object.keys(payloadAfter).length, 0); - }); - - it("should work ok for not existing handle", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const res = await Session.removeClaim("asfd", TrueClaim); - assert.equal(res, false); - }); - }); -}); diff --git a/test/session/claims/removeClaim.test.ts b/test/session/claims/removeClaim.test.ts new file mode 100644 index 000000000..15852e61c --- /dev/null +++ b/test/session/claims/removeClaim.test.ts @@ -0,0 +1,181 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import SessionClass from 'supertokens-node/recipe/session/sessionClass' +import sinon from 'sinon' +import { ProcessState } from 'supertokens-node/processState' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, mockRequest, mockResponse, printPath, setupST, startST } from '../../utils' +import { TrueClaim } from './testClaims' + +describe(`sessionClaims/removeClaim: ${printPath('[test/session/claims/removeClaim.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('SessionClass.removeClaim', () => { + afterEach(() => { + sinon.restore() + }) + + it('should attempt to set claim to null', async () => { + const session = new SessionClass({}, 'testToken', 'testHandle', 'testUserId', {}, {}) + sinon.useFakeTimers() + const mock = sinon.mock(session).expects('mergeIntoAccessTokenPayload').once().withArgs({ + 'st-true': null, + }) + await session.removeClaim(TrueClaim) + mock.verify() + }) + + it('should clear previously set claim', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const response = mockResponse() + const res = await Session.createNewSession(mockRequest(), response, 'someId') + + const payload = res.getAccessTokenPayload() + assert.equal(Object.keys(payload).length, 1) + assert.ok(payload['st-true']) + assert.equal(payload['st-true'].v, true) + assert(payload['st-true'].t > Date.now() - 10000) + + await res.removeClaim(TrueClaim) + + const payloadAfter = res.getAccessTokenPayload() + assert.equal(Object.keys(payloadAfter).length, 0) + }) + + it('should clear previously set claim using a handle', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const response = mockResponse() + const session = await Session.createNewSession(mockRequest(), response, 'someId') + + const payload = session.getAccessTokenPayload() + assert.equal(Object.keys(payload).length, 1) + assert.ok(payload['st-true']) + assert.equal(payload['st-true'].v, true) + assert(payload['st-true'].t > Date.now() - 10000) + + const res = await Session.removeClaim(session.getHandle(), TrueClaim) + assert.equal(res, true) + + const payloadAfter = (await Session.getSessionInformation(session.getHandle())).accessTokenPayload + assert.equal(Object.keys(payloadAfter).length, 0) + }) + + it('should work ok for not existing handle', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const res = await Session.removeClaim('asfd', TrueClaim) + assert.equal(res, false) + }) + }) +}) diff --git a/test/session/claims/setClaimValue.test.js b/test/session/claims/setClaimValue.test.js deleted file mode 100644 index cee0e94e8..000000000 --- a/test/session/claims/setClaimValue.test.js +++ /dev/null @@ -1,175 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, startST, killAllST, setupST, cleanST, mockResponse, mockRequest } = require("../../utils"); -const assert = require("assert"); -const SuperTokens = require("../../.."); -const Session = require("../../../recipe/session"); -const { default: SessionClass } = require("../../../lib/build/recipe/session/sessionClass"); -const sinon = require("sinon"); -const { TrueClaim } = require("./testClaims"); -const { ProcessState } = require("../../../lib/build/processState"); - -describe(`sessionClaims/setClaimValue: ${printPath("[test/session/claims/setClaimValue.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("SessionClass.setClaimValue", () => { - afterEach(() => { - sinon.restore(); - }); - - it("should merge the right value", async () => { - const session = new SessionClass({}, "testToken", "testHandle", "testUserId", {}, {}); - sinon.useFakeTimers(); - const mock = sinon - .mock(session) - .expects("mergeIntoAccessTokenPayload") - .once() - .withArgs({ - "st-true": { - t: 0, - v: true, - }, - }); - await session.setClaimValue(TrueClaim, true); - mock.verify(); - }); - - it("should overwrite claim value", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "someId"); - - const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 1); - assert.ok(payload["st-true"]); - assert.equal(payload["st-true"].v, true); - assert(payload["st-true"].t > Date.now() - 2000); - - await res.setClaimValue(TrueClaim, false); - - const payloadAfter = res.getAccessTokenPayload(); - assert.equal(Object.keys(payloadAfter).length, 1); - assert.ok(payloadAfter["st-true"]); - assert.equal(payloadAfter["st-true"].v, false); - assert(payloadAfter["st-true"].t > payload["st-true"].t); - }); - - it("should overwrite claim value using session handle", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const response = mockResponse(); - const res = await Session.createNewSession(mockRequest(), response, "someId"); - - const payload = res.getAccessTokenPayload(); - assert.equal(Object.keys(payload).length, 1); - assert.ok(payload["st-true"]); - assert.equal(payload["st-true"].v, true); - assert(payload["st-true"].t > Date.now() - 10000); - - await Session.setClaimValue(res.getHandle(), TrueClaim, false); - - const payloadAfter = (await Session.getSessionInformation(res.getHandle())).accessTokenPayload; - assert.equal(Object.keys(payloadAfter).length, 1); - assert.ok(payloadAfter["st-true"]); - assert.equal(payloadAfter["st-true"].v, false); - assert(payloadAfter["st-true"].t > payload["st-true"].t); - }); - - it("should work ok for not existing handle", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const res = await Session.setClaimValue("asfd", TrueClaim, false); - assert.equal(res, false); - }); - }); -}); diff --git a/test/session/claims/setClaimValue.test.ts b/test/session/claims/setClaimValue.test.ts new file mode 100644 index 000000000..5c533cf82 --- /dev/null +++ b/test/session/claims/setClaimValue.test.ts @@ -0,0 +1,177 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import SessionClass from 'supertokens-node/recipe/session/sessionClass' +import sinon from 'sinon' +import { ProcessState } from 'supertokens-node/processState' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, mockRequest, mockResponse, printPath, setupST, startST } from '../../utils' +import { TrueClaim } from './testClaims' + +describe(`sessionClaims/setClaimValue: ${printPath('[test/session/claims/setClaimValue.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('SessionClass.setClaimValue', () => { + afterEach(() => { + sinon.restore() + }) + + it('should merge the right value', async () => { + const session = new SessionClass({}, 'testToken', 'testHandle', 'testUserId', {}, {}) + sinon.useFakeTimers() + const mock = sinon + .mock(session) + .expects('mergeIntoAccessTokenPayload') + .once() + .withArgs({ + 'st-true': { + t: 0, + v: true, + }, + }) + await session.setClaimValue(TrueClaim, true) + mock.verify() + }) + + it('should overwrite claim value', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const response = mockResponse() + const res = await Session.createNewSession(mockRequest(), response, 'someId') + + const payload = res.getAccessTokenPayload() + assert.equal(Object.keys(payload).length, 1) + assert.ok(payload['st-true']) + assert.equal(payload['st-true'].v, true) + assert(payload['st-true'].t > Date.now() - 2000) + + await res.setClaimValue(TrueClaim, false) + + const payloadAfter = res.getAccessTokenPayload() + assert.equal(Object.keys(payloadAfter).length, 1) + assert.ok(payloadAfter['st-true']) + assert.equal(payloadAfter['st-true'].v, false) + assert(payloadAfter['st-true'].t > payload['st-true'].t) + }) + + it('should overwrite claim value using session handle', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const response = mockResponse() + const res = await Session.createNewSession(mockRequest(), response, 'someId') + + const payload = res.getAccessTokenPayload() + assert.equal(Object.keys(payload).length, 1) + assert.ok(payload['st-true']) + assert.equal(payload['st-true'].v, true) + assert(payload['st-true'].t > Date.now() - 10000) + + await Session.setClaimValue(res.getHandle(), TrueClaim, false) + + const payloadAfter = (await Session.getSessionInformation(res.getHandle())).accessTokenPayload + assert.equal(Object.keys(payloadAfter).length, 1) + assert.ok(payloadAfter['st-true']) + assert.equal(payloadAfter['st-true'].v, false) + assert(payloadAfter['st-true'].t > payload['st-true'].t) + }) + + it('should work ok for not existing handle', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const res = await Session.setClaimValue('asfd', TrueClaim, false) + assert.equal(res, false) + }) + }) +}) diff --git a/test/session/claims/testClaims.js b/test/session/claims/testClaims.js deleted file mode 100644 index 3c8b44971..000000000 --- a/test/session/claims/testClaims.js +++ /dev/null @@ -1,42 +0,0 @@ -const Sinon = require("sinon"); -const { BooleanClaim } = require("../../../lib/build/recipe/session/claimBaseClasses/booleanClaim"); -const { PrimitiveClaim } = require("../../../lib/build/recipe/session/claimBaseClasses/primitiveClaim"); - -module.exports.TrueClaim = new BooleanClaim({ - key: "st-true", - fetchValue: () => true, -}); - -module.exports.UndefinedClaim = new BooleanClaim({ - key: "st-undef", - fetchValue: () => undefined, -}); - -module.exports.StubClaim = class StubClaim extends PrimitiveClaim { - constructor({ key, fetchValue, fetchValueRes, id, validate, validateRes, shouldRefetch, shouldRefetchRes }) { - super(key); - this.fetchValue = Sinon.stub(); - if (fetchValue) { - this.fetchValue.callsFake(fetchValue); - } else { - this.fetchValue.resolves(fetchValueRes); - } - - this.validators.stub = { - id, - }; - if (shouldRefetch !== undefined || shouldRefetchRes !== undefined) { - this.validators.stub.claim = this; - if (shouldRefetch !== undefined) { - this.validators.stub.shouldRefetch = Sinon.stub().callsFake(shouldRefetch); - } else { - this.validators.stub.shouldRefetch = Sinon.stub().resolves(shouldRefetchRes); - } - } - if (validate) { - this.validators.stub.validate = Sinon.stub().callsFake(validate); - } else { - this.validators.stub.validate = Sinon.stub().resolves(validateRes); - } - } -}; diff --git a/test/session/claims/testClaims.ts b/test/session/claims/testClaims.ts new file mode 100644 index 000000000..0a9ca0568 --- /dev/null +++ b/test/session/claims/testClaims.ts @@ -0,0 +1,42 @@ +import Sinon from 'sinon' +import { BooleanClaim } from 'supertokens-node/recipe/session/claimBaseClasses/booleanClaim' +import { PrimitiveClaim } from 'supertokens-node/recipe/session/claimBaseClasses/primitiveClaim' + +export const UndefinedClaim = new BooleanClaim({ + key: 'st-undef', + fetchValue: () => undefined, +}) + +export const TrueClaim = new BooleanClaim({ + key: 'st-true', + fetchValue: () => true, +}) + +export class StubClaim extends PrimitiveClaim { + constructor({ key, fetchValue, fetchValueRes, id, validate, validateRes, shouldRefetch, shouldRefetchRes }) { + super(key) + this.fetchValue = Sinon.stub() + if (fetchValue) + this.fetchValue.callsFake(fetchValue) + + else + this.fetchValue.resolves(fetchValueRes) + + this.validators.stub = { + id, + } + if (shouldRefetch !== undefined || shouldRefetchRes !== undefined) { + this.validators.stub.claim = this + if (shouldRefetch !== undefined) + this.validators.stub.shouldRefetch = Sinon.stub().callsFake(shouldRefetch) + + else + this.validators.stub.shouldRefetch = Sinon.stub().resolves(shouldRefetchRes) + } + if (validate) + this.validators.stub.validate = Sinon.stub().callsFake(validate) + + else + this.validators.stub.validate = Sinon.stub().resolves(validateRes) + } +} diff --git a/test/session/claims/validateClaimsForSessionHandle.test.js b/test/session/claims/validateClaimsForSessionHandle.test.js deleted file mode 100644 index bb736c7e7..000000000 --- a/test/session/claims/validateClaimsForSessionHandle.test.js +++ /dev/null @@ -1,118 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, startST, killAllST, setupST, cleanST, mockResponse, mockRequest } = require("../../utils"); -const assert = require("assert"); -const SuperTokens = require("../../.."); -const Session = require("../../../recipe/session"); -const sinon = require("sinon"); -const { TrueClaim, UndefinedClaim } = require("./testClaims"); -const { ProcessState } = require("../../../lib/build/processState"); - -describe(`sessionClaims/validateClaimsForSessionHandle: ${printPath( - "[test/session/claims/validateClaimsForSessionHandle.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("Session.validateClaimsForSessionHandle", () => { - afterEach(() => { - sinon.restore(); - }); - - it("should return the right validation errors", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - }), - ], - }); - - const response = mockResponse(); - const session = await Session.createNewSession(mockRequest(), response, "someId"); - - const failingValidator = UndefinedClaim.validators.hasValue(true); - assert.deepStrictEqual( - await Session.validateClaimsForSessionHandle(session.getHandle(), () => [ - TrueClaim.validators.hasValue(true), - failingValidator, - ]), - { - status: "OK", - invalidClaims: [ - { - id: failingValidator.id, - reason: { - actualValue: undefined, - expectedValue: true, - message: "value does not exist", - }, - }, - ], - } - ); - }); - - it("should work for not existing handle", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - assert.deepStrictEqual(await Session.validateClaimsForSessionHandle("asfd"), { - status: "SESSION_DOES_NOT_EXIST_ERROR", - }); - }); - }); -}); diff --git a/test/session/claims/validateClaimsForSessionHandle.test.ts b/test/session/claims/validateClaimsForSessionHandle.test.ts new file mode 100644 index 000000000..db26d1a51 --- /dev/null +++ b/test/session/claims/validateClaimsForSessionHandle.test.ts @@ -0,0 +1,120 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import sinon from 'sinon' +import { ProcessState } from 'supertokens-node/processState' +import { afterAll, afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, mockRequest, mockResponse, printPath, setupST, startST } from '../../utils' +import { TrueClaim, UndefinedClaim } from './testClaims' + +describe(`sessionClaims/validateClaimsForSessionHandle: ${printPath( + '[test/session/claims/validateClaimsForSessionHandle.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('Session.validateClaimsForSessionHandle', () => { + afterEach(() => { + sinon.restore() + }) + + it('should return the right validation errors', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + }), + ], + }) + + const response = mockResponse() + const session = await Session.createNewSession(mockRequest(), response, 'someId') + + const failingValidator = UndefinedClaim.validators.hasValue(true) + assert.deepStrictEqual( + await Session.validateClaimsForSessionHandle(session.getHandle(), () => [ + TrueClaim.validators.hasValue(true), + failingValidator, + ]), + { + status: 'OK', + invalidClaims: [ + { + id: failingValidator.id, + reason: { + actualValue: undefined, + expectedValue: true, + message: 'value does not exist', + }, + }, + ], + }, + ) + }) + + it('should work for not existing handle', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + assert.deepStrictEqual(await Session.validateClaimsForSessionHandle('asfd'), { + status: 'SESSION_DOES_NOT_EXIST_ERROR', + }) + }) + }) +}) diff --git a/test/session/claims/verifySession.test.js b/test/session/claims/verifySession.test.js deleted file mode 100644 index 6a7197cc7..000000000 --- a/test/session/claims/verifySession.test.js +++ /dev/null @@ -1,719 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse } = require("../../utils"); -const assert = require("assert"); -const { ProcessState } = require("../../../lib/build/processState"); -const SuperTokens = require("../../../"); -const Session = require("../../../recipe/session"); -const { default: SessionClass } = require("../../../lib/build/recipe/session/sessionClass"); -const { verifySession } = require("../../../recipe/session/framework/express"); -const { middleware, errorHandler } = require("../../../framework/express"); -const { PrimitiveClaim } = require("../../../lib/build/recipe/session/claimBaseClasses/primitiveClaim"); -const express = require("express"); -const request = require("supertest"); -const { TrueClaim, UndefinedClaim } = require("./testClaims"); -const sinon = require("sinon"); -const { default: SessionError } = require("../../../lib/build/recipe/session/error"); - -describe(`sessionClaims/verifySession: ${printPath("[test/session/claims/verifySession.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - afterEach(function () { - sinon.restore(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("verifySession", () => { - describe("with getGlobalClaimValidators override", () => { - it("should allow without claims required or present", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = getTestApp(); - - const session = await createSession(app); - await testGet(app, session, "/default-claims", 200); - }); - - it("should allow with claim valid after refetching", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - TrueClaim.validators.hasValue(true), - ], - }), - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const session = await createSession(app); - await testGet(app, session, "/default-claims", 200); - }); - - it("should reject with claim required but not added", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - UndefinedClaim.validators.hasValue(true), - ], - }), - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const session = await createSession(app); - const resp = await testGet(app, session, "/default-claims", 403); - - validateErrorResp(resp, [ - { - id: "st-undef", - reason: { - message: "value does not exist", - expectedValue: true, - }, - }, - ]); - }); - - it("should allow with custom validator returning true", async function () { - await startST(); - const customValidator = { - id: "testid", - validate: () => ({ isValid: true }), - }; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - customValidator, - ], - }), - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const session = await createSession(app); - await testGet(app, session, "/default-claims", 200); - }); - - it("should reject with custom validator returning false", async function () { - await startST(); - const customValidator = { - id: "testid", - validate: () => ({ isValid: false }), - }; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - customValidator, - ], - }), - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const session = await createSession(app); - const resp = await testGet(app, session, "/default-claims", 403); - - validateErrorResp(resp, [{ id: "testid" }]); - }); - - it("should reject with validator returning false with reason", async function () { - await startST(); - const customValidator = { - id: "testid", - validate: () => ({ isValid: false, reason: "testReason" }), - }; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - customValidator, - ], - }), - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = getTestApp(); - - const session = await createSession(app); - const resp = await testGet(app, session, "/default-claims", 403); - - validateErrorResp(resp, [{ id: "testid", reason: "testReason" }]); - }); - - it("should reject if assertClaims returns an error", async function () { - const obj = {}; - const testValidatorArr = [obj]; - - const validateClaims = sinon.expectation - .create("validateClaims") - .once() - .withArgs({ - accessTokenPayload: {}, - userId: "testing-userId", - claimValidators: testValidatorArr, - userContext: { - _default: { - request: sinon.match.any, - }, - }, - }) - .resolves({ - invalidClaims: [ - { - id: "testid", - reason: "testReason", - }, - ], - }); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: () => testValidatorArr, - validateClaims, - }), - }, - }), - ], - }); - - const app = getTestApp(); - - const session = await createSession(app); - - const res = await testGet(app, session, "/default-claims", 403); - validateErrorResp(res, [{ id: "testid", reason: "testReason" }]); - - validateClaims.verify(); - }); - - it("should allow if assertClaims returns no errors", async function () { - const obj = {}; - const testValidatorArr = [obj]; - const validateClaims = sinon.expectation - .create("validateClaims") - .once() - .withArgs({ - accessTokenPayload: {}, - userId: "testing-userId", - claimValidators: testValidatorArr, - userContext: { - _default: { - request: sinon.match.any, - }, - }, - }) - .resolves({ - invalidClaims: [], - }); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: () => testValidatorArr, - validateClaims, - }), - }, - }), - ], - }); - - const app = getTestApp(); - - const session = await createSession(app); - - await testGet(app, session, "/default-claims", 200); - validateClaims.verify(); - }); - }); - - describe("with overrideGlobalClaimValidators", () => { - it("should allow with empty list as override", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - UndefinedClaim, - ], - }), - }, - }), - ], - }); - - const app = getTestApp([ - { - path: "/no-claims", - overrideGlobalClaimValidators: () => [], - }, - ]); - - const session = await createSession(app); - await testGet(app, session, "/no-claims", 200); - }); - - it("should allow with refetched claim", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - UndefinedClaim, - ], - }), - }, - }), - ], - }); - - const app = getTestApp([ - { - path: "/refetched-claim", - overrideGlobalClaimValidators: () => [TrueClaim.validators.hasValue(true)], - }, - ]); - - const session = await createSession(app); - await testGet(app, session, "/refetched-claim", 200); - }); - - it("should reject with invalid refetched claim", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - UndefinedClaim, - ], - }), - }, - }), - ], - }); - - const app = getTestApp([ - { - path: "/refetched-claim", - overrideGlobalClaimValidators: () => [TrueClaim.validators.hasValue(false)], - }, - ]); - - const session = await createSession(app); - const res = await testGet(app, session, "/refetched-claim", 403); - validateErrorResp(res, [ - { id: "st-true", reason: { message: "wrong value", expectedValue: false, actualValue: true } }, - ]); - }); - - it("should reject with custom claim returning false", async function () { - const customValidator = { - id: "testid", - validate: () => ({ isValid: false, reason: "testReason" }), - }; - - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - id: "testid", - reason: "testReason", - }, - ], - }), - }, - }), - ], - }); - - const app = getTestApp([ - { - path: "/refetched-claim", - overrideGlobalClaimValidators: () => [customValidator], - }, - ]); - - const session = await createSession(app); - const res = await testGet(app, session, "/refetched-claim", 403); - validateErrorResp(res, [{ id: "testid", reason: "testReason" }]); - }); - - it("should allow with custom claim returning true", async function () { - const customValidator = { - id: "testid", - validate: () => ({ isValid: true }), - }; - - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - id: "testid", - reason: "testReason", - }, - ], - }), - }, - }), - ], - }); - - const app = getTestApp([ - { - path: "/refetched-claim", - overrideGlobalClaimValidators: () => [customValidator], - }, - ]); - - const session = await createSession(app); - await testGet(app, session, "/refetched-claim", 200); - }); - - it("should reject with custom claim returning false", async function () { - const customValidator = { - id: "testid", - validate: () => ({ isValid: false, reason: "testReason" }), - }; - - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => ({ - ...oI, - - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - id: "testid", - reason: "testReason", - }, - ], - }), - }, - }), - ], - }); - - const app = getTestApp([ - { - path: "/refetched-claim", - overrideGlobalClaimValidators: () => [customValidator], - }, - ]); - - const session = await createSession(app); - const res = await testGet(app, session, "/refetched-claim", 403); - validateErrorResp(res, [{ id: "testid", reason: "testReason" }]); - }); - }); - }); -}); - -function validateErrorResp(resp, errors) { - assert.ok(resp.body); - assert.strictEqual(resp.body.message, "invalid claim"); - assert.deepStrictEqual(resp.body.claimValidationErrors, errors); -} - -async function createSession(app, body) { - return extractInfoFromResponse( - await new Promise((resolve, reject) => - request(app) - .post(body !== undefined ? "create-with-claim" : "/create") - .send(body) - .expect(200) - .end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }) - ) - ); -} - -function testGet(app, info, url, expectedStatus) { - return new Promise((resolve, reject) => - request(app) - .get(url) - .set("Cookie", ["sAccessToken=" + info.accessToken]) - .set("anti-csrf", info.antiCsrf) - .expect(expectedStatus) - .end((err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }) - ); -} - -function getTestApp(endpoints) { - const app = express(); - - app.use(middleware()); - - app.use(express.json()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "testing-userId", req.body, {}); - res.status(200).json({ message: true }); - }); - - app.get("/default-claims", verifySession(), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - - if (endpoints !== undefined) { - for (const { path, overrideGlobalClaimValidators } of endpoints) { - app.get(path, verifySession({ overrideGlobalClaimValidators }), async (req, res) => { - res.status(200).json({ message: req.session.getHandle() }); - }); - } - } - - app.post("/logout", verifySession(), async (req, res) => { - await req.session.revokeSession(); - res.status(200).json({ message: true }); - }); - - app.use(errorHandler()); - return app; -} diff --git a/test/session/claims/verifySession.test.ts b/test/session/claims/verifySession.test.ts new file mode 100644 index 000000000..f787be37f --- /dev/null +++ b/test/session/claims/verifySession.test.ts @@ -0,0 +1,718 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { verifySession } from 'supertokens-node/recipe/session/framework/express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import express from 'express' +import request from 'supertest' +import sinon from 'sinon' +import { afterEach, beforeEach, describe, it } from 'vitest' +import { cleanST, extractInfoFromResponse, killAllST, printPath, setupST, startST } from '../../utils' +import { TrueClaim, UndefinedClaim } from './testClaims' + +describe(`sessionClaims/verifySession: ${printPath('[test/session/claims/verifySession.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterEach(() => { + sinon.restore() + }) + + afterEach(async () => { + await killAllST() + await cleanST() + }) + + describe('verifySession', () => { + describe('with getGlobalClaimValidators override', () => { + it('should allow without claims required or present', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = getTestApp() + + const session = await createSession(app) + await testGet(app, session, '/default-claims', 200) + }) + + it('should allow with claim valid after refetching', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + TrueClaim.validators.hasValue(true), + ], + }), + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const session = await createSession(app) + await testGet(app, session, '/default-claims', 200) + }) + + it('should reject with claim required but not added', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + UndefinedClaim.validators.hasValue(true), + ], + }), + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const session = await createSession(app) + const resp = await testGet(app, session, '/default-claims', 403) + + validateErrorResp(resp, [ + { + id: 'st-undef', + reason: { + message: 'value does not exist', + expectedValue: true, + }, + }, + ]) + }) + + it('should allow with custom validator returning true', async () => { + await startST() + const customValidator = { + id: 'testid', + validate: () => ({ isValid: true }), + } + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + customValidator, + ], + }), + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const session = await createSession(app) + await testGet(app, session, '/default-claims', 200) + }) + + it('should reject with custom validator returning false', async () => { + await startST() + const customValidator = { + id: 'testid', + validate: () => ({ isValid: false }), + } + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + customValidator, + ], + }), + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const session = await createSession(app) + const resp = await testGet(app, session, '/default-claims', 403) + + validateErrorResp(resp, [{ id: 'testid' }]) + }) + + it('should reject with validator returning false with reason', async () => { + await startST() + const customValidator = { + id: 'testid', + validate: () => ({ isValid: false, reason: 'testReason' }), + } + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + customValidator, + ], + }), + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = getTestApp() + + const session = await createSession(app) + const resp = await testGet(app, session, '/default-claims', 403) + + validateErrorResp(resp, [{ id: 'testid', reason: 'testReason' }]) + }) + + it('should reject if assertClaims returns an error', async () => { + const obj = {} + const testValidatorArr = [obj] + + const validateClaims = sinon.expectation + .create('validateClaims') + .once() + .withArgs({ + accessTokenPayload: {}, + userId: 'testing-userId', + claimValidators: testValidatorArr, + userContext: { + _default: { + request: sinon.match.any, + }, + }, + }) + .resolves({ + invalidClaims: [ + { + id: 'testid', + reason: 'testReason', + }, + ], + }) + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: () => testValidatorArr, + validateClaims, + }), + }, + }), + ], + }) + + const app = getTestApp() + + const session = await createSession(app) + + const res = await testGet(app, session, '/default-claims', 403) + validateErrorResp(res, [{ id: 'testid', reason: 'testReason' }]) + + validateClaims.verify() + }) + + it('should allow if assertClaims returns no errors', async () => { + const obj = {} + const testValidatorArr = [obj] + const validateClaims = sinon.expectation + .create('validateClaims') + .once() + .withArgs({ + accessTokenPayload: {}, + userId: 'testing-userId', + claimValidators: testValidatorArr, + userContext: { + _default: { + request: sinon.match.any, + }, + }, + }) + .resolves({ + invalidClaims: [], + }) + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: () => testValidatorArr, + validateClaims, + }), + }, + }), + ], + }) + + const app = getTestApp() + + const session = await createSession(app) + + await testGet(app, session, '/default-claims', 200) + validateClaims.verify() + }) + }) + + describe('with overrideGlobalClaimValidators', () => { + it('should allow with empty list as override', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + UndefinedClaim, + ], + }), + }, + }), + ], + }) + + const app = getTestApp([ + { + path: '/no-claims', + overrideGlobalClaimValidators: () => [], + }, + ]) + + const session = await createSession(app) + await testGet(app, session, '/no-claims', 200) + }) + + it('should allow with refetched claim', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + UndefinedClaim, + ], + }), + }, + }), + ], + }) + + const app = getTestApp([ + { + path: '/refetched-claim', + overrideGlobalClaimValidators: () => [TrueClaim.validators.hasValue(true)], + }, + ]) + + const session = await createSession(app) + await testGet(app, session, '/refetched-claim', 200) + }) + + it('should reject with invalid refetched claim', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + UndefinedClaim, + ], + }), + }, + }), + ], + }) + + const app = getTestApp([ + { + path: '/refetched-claim', + overrideGlobalClaimValidators: () => [TrueClaim.validators.hasValue(false)], + }, + ]) + + const session = await createSession(app) + const res = await testGet(app, session, '/refetched-claim', 403) + validateErrorResp(res, [ + { id: 'st-true', reason: { message: 'wrong value', expectedValue: false, actualValue: true } }, + ]) + }) + + it('should reject with custom claim returning false', async () => { + const customValidator = { + id: 'testid', + validate: () => ({ isValid: false, reason: 'testReason' }), + } + + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + id: 'testid', + reason: 'testReason', + }, + ], + }), + }, + }), + ], + }) + + const app = getTestApp([ + { + path: '/refetched-claim', + overrideGlobalClaimValidators: () => [customValidator], + }, + ]) + + const session = await createSession(app) + const res = await testGet(app, session, '/refetched-claim', 403) + validateErrorResp(res, [{ id: 'testid', reason: 'testReason' }]) + }) + + it('should allow with custom claim returning true', async () => { + const customValidator = { + id: 'testid', + validate: () => ({ isValid: true }), + } + + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + id: 'testid', + reason: 'testReason', + }, + ], + }), + }, + }), + ], + }) + + const app = getTestApp([ + { + path: '/refetched-claim', + overrideGlobalClaimValidators: () => [customValidator], + }, + ]) + + const session = await createSession(app) + await testGet(app, session, '/refetched-claim', 200) + }) + + it('should reject with custom claim returning false', async () => { + const customValidator = { + id: 'testid', + validate: () => ({ isValid: false, reason: 'testReason' }), + } + + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: oI => ({ + ...oI, + + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + id: 'testid', + reason: 'testReason', + }, + ], + }), + }, + }), + ], + }) + + const app = getTestApp([ + { + path: '/refetched-claim', + overrideGlobalClaimValidators: () => [customValidator], + }, + ]) + + const session = await createSession(app) + const res = await testGet(app, session, '/refetched-claim', 403) + validateErrorResp(res, [{ id: 'testid', reason: 'testReason' }]) + }) + }) + }) +}) + +function validateErrorResp(resp, errors) { + assert.ok(resp.body) + assert.strictEqual(resp.body.message, 'invalid claim') + assert.deepStrictEqual(resp.body.claimValidationErrors, errors) +} + +async function createSession(app, body) { + return extractInfoFromResponse( + await new Promise((resolve, reject) => + request(app) + .post(body !== undefined ? 'create-with-claim' : '/create') + .send(body) + .expect(200) + .end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }), + ), + ) +} + +function testGet(app, info, url, expectedStatus) { + return new Promise((resolve, reject) => + request(app) + .get(url) + .set('Cookie', [`sAccessToken=${info.accessToken}`]) + .set('anti-csrf', info.antiCsrf) + .expect(expectedStatus) + .end((err, res) => { + if (err) + reject(err) + + else + resolve(res) + }), + ) +} + +function getTestApp(endpoints) { + const app = express() + + app.use(middleware()) + + app.use(express.json()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'testing-userId', req.body, {}) + res.status(200).json({ message: true }) + }) + + app.get('/default-claims', verifySession(), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + + if (endpoints !== undefined) { + for (const { path, overrideGlobalClaimValidators } of endpoints) { + app.get(path, verifySession({ overrideGlobalClaimValidators }), async (req, res) => { + res.status(200).json({ message: req.session.getHandle() }) + }) + } + } + + app.post('/logout', verifySession(), async (req, res) => { + await req.session.revokeSession() + res.status(200).json({ message: true }) + }) + + app.use(errorHandler()) + return app +} diff --git a/test/session/claims/withJWT.test.js b/test/session/claims/withJWT.test.js deleted file mode 100644 index b10c9fa02..000000000 --- a/test/session/claims/withJWT.test.js +++ /dev/null @@ -1,145 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const JsonWebToken = require("jsonwebtoken"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -let { Querier } = require("../../../lib/build/querier"); -let { maxVersion } = require("../../../lib/build/utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState } = require("../../../lib/build/processState"); -let SuperTokens = require("../../../"); -let Session = require("../../../recipe/session"); -let { middleware, errorHandler } = require("../../../framework/express"); -const { TrueClaim, UndefinedClaim } = require("./testClaims"); - -describe(`sessionClaims/withJWT: ${printPath("[test/session/claims/withJWT.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("JWT + claims interaction", () => { - it("should create the right access token payload with claims and JWT enabled", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => ({ - ...oI, - createNewSession: async (input) => { - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...(await TrueClaim.build(input.userId, input.userContext)), - }; - return oI.createNewSession(input); - }, - }), - }, - jwt: { enable: true }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", undefined, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - - const sessionInfo = await Session.getSessionInformation(sessionHandle); - let accessTokenPayload = sessionInfo.accessTokenPayload; - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - - assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), true); - assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), true); - - const failingValidator = UndefinedClaim.validators.hasValue(true); - assert.deepStrictEqual( - await Session.validateClaimsInJWTPayload(sessionInfo.userId, decodedJWT, () => [ - TrueClaim.validators.hasValue(true, 2), - failingValidator, - ]), - { - status: "OK", - invalidClaims: [ - { - id: failingValidator.id, - reason: { - actualValue: undefined, - expectedValue: true, - message: "value does not exist", - }, - }, - ], - } - ); - }); - }); -}); diff --git a/test/session/claims/withJWT.test.ts b/test/session/claims/withJWT.test.ts new file mode 100644 index 000000000..21206ec74 --- /dev/null +++ b/test/session/claims/withJWT.test.ts @@ -0,0 +1,145 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import JsonWebToken from 'jsonwebtoken' +import express from 'express' +import request from 'supertest' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' +import { TrueClaim, UndefinedClaim } from './testClaims' + +describe(`sessionClaims/withJWT: ${printPath('[test/session/claims/withJWT.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('JWT + claims interaction', () => { + it('should create the right access token payload with claims and JWT enabled', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: oI => ({ + ...oI, + createNewSession: async (input) => { + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...(await TrueClaim.build(input.userId, input.userContext)), + } + return oI.createNewSession(input) + }, + }), + }, + jwt: { enable: true }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', undefined, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + + const sessionInfo = await Session.getSessionInformation(sessionHandle) + const accessTokenPayload = sessionInfo.accessTokenPayload + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + + assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), true) + assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), true) + + const failingValidator = UndefinedClaim.validators.hasValue(true) + assert.deepStrictEqual( + await Session.validateClaimsInJWTPayload(sessionInfo.userId, decodedJWT, () => [ + TrueClaim.validators.hasValue(true, 2), + failingValidator, + ]), + { + status: 'OK', + invalidClaims: [ + { + id: failingValidator.id, + reason: { + actualValue: undefined, + expectedValue: true, + message: 'value does not exist', + }, + }, + ], + }, + ) + }) + }) +}) diff --git a/test/session/with-jwt/jwt.override.test.js b/test/session/with-jwt/jwt.override.test.js deleted file mode 100644 index 7da217e01..000000000 --- a/test/session/with-jwt/jwt.override.test.js +++ /dev/null @@ -1,214 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -let { Querier } = require("../../../lib/build/querier"); -let { maxVersion } = require("../../../lib/build/utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState } = require("../../../lib/build/processState"); -let SuperTokens = require("../../../"); -let Session = require("../../../recipe/session"); -let { middleware, errorHandler } = require("../../../framework/express"); - -/** - * Test that overriding the jwt recipe functions and apis still work when the JWT feature is enabled - */ -describe(`session-with-jwt: ${printPath("[test/session/with-jwt/jwt.override.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test overriding functions", async function () { - await startST(); - - let jwtCreated = undefined; - let jwksKeys = undefined; - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - openIdFeature: { - jwtFeature: { - functions: function (originalImplementation) { - return { - ...originalImplementation, - createJWT: async function (input) { - let createJWTResponse = await originalImplementation.createJWT(input); - - if (createJWTResponse.status === "OK") { - jwtCreated = createJWTResponse.jwt; - } - - return createJWTResponse; - }, - getJWKS: async function () { - let getJWKSResponse = await originalImplementation.getJWKS(); - - if (getJWKSResponse.status === "OK") { - jwksKeys = getJWKSResponse.keys; - } - - return getJWKSResponse; - }, - }; - }, - }, - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - let sessionHandle = createJWTResponse.sessionHandle; - assert.notStrictEqual(jwtCreated, undefined); - - let sessionInformation = await Session.getSessionInformation(sessionHandle); - assert.deepStrictEqual(jwtCreated, sessionInformation.accessTokenPayload.jwt); - - let getJWKSResponse = await new Promise((resolve) => { - request(app) - .get("/auth/jwt/jwks.json") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }); - }); - - assert.notStrictEqual(jwksKeys, undefined); - assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys); - }); - - it("Test overriding APIs", async function () { - await startST(); - - let jwksKeys = undefined; - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - openIdFeature: { - jwtFeature: { - apis: function (originalImplementation) { - return { - ...originalImplementation, - getJWKSGET: async function (input) { - let response = await originalImplementation.getJWKSGET(input); - jwksKeys = response.keys; - return response; - }, - }; - }, - }, - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - - let getJWKSResponse = await new Promise((resolve) => { - request(app) - .get("/auth/jwt/jwks.json") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }); - }); - - app.use(errorHandler()); - - assert.notStrictEqual(jwksKeys, undefined); - assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys); - }); -}); diff --git a/test/session/with-jwt/jwt.override.test.ts b/test/session/with-jwt/jwt.override.test.ts new file mode 100644 index 000000000..e4df8f64d --- /dev/null +++ b/test/session/with-jwt/jwt.override.test.ts @@ -0,0 +1,211 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import express from 'express' +import request from 'supertest' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +/** + * Test that overriding the jwt recipe functions and apis still work when the JWT feature is enabled + */ +describe(`session-with-jwt: ${printPath('[test/session/with-jwt/jwt.override.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test overriding functions', async () => { + await startST() + + let jwtCreated + let jwksKeys + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + openIdFeature: { + jwtFeature: { + functions(originalImplementation) { + return { + ...originalImplementation, + async createJWT(input) { + const createJWTResponse = await originalImplementation.createJWT(input) + + if (createJWTResponse.status === 'OK') + jwtCreated = createJWTResponse.jwt + + return createJWTResponse + }, + async getJWKS() { + const getJWKSResponse = await originalImplementation.getJWKS() + + if (getJWKSResponse.status === 'OK') + jwksKeys = getJWKSResponse.keys + + return getJWKSResponse + }, + } + }, + }, + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, '', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + const sessionHandle = createJWTResponse.sessionHandle + assert.notStrictEqual(jwtCreated, undefined) + + const sessionInformation = await Session.getSessionInformation(sessionHandle) + assert.deepStrictEqual(jwtCreated, sessionInformation.accessTokenPayload.jwt) + + const getJWKSResponse = await new Promise((resolve) => { + request(app) + .get('/auth/jwt/jwks.json') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }) + }) + + assert.notStrictEqual(jwksKeys, undefined) + assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys) + }) + + it('Test overriding APIs', async () => { + await startST() + + let jwksKeys + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + openIdFeature: { + jwtFeature: { + apis(originalImplementation) { + return { + ...originalImplementation, + async getJWKSGET(input) { + const response = await originalImplementation.getJWKSGET(input) + jwksKeys = response.keys + return response + }, + } + }, + }, + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + const getJWKSResponse = await new Promise((resolve) => { + request(app) + .get('/auth/jwt/jwks.json') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }) + }) + + app.use(errorHandler()) + + assert.notStrictEqual(jwksKeys, undefined) + assert.deepStrictEqual(jwksKeys, getJWKSResponse.keys) + }) +}) diff --git a/test/session/with-jwt/jwtFunctions.test.js b/test/session/with-jwt/jwtFunctions.test.js deleted file mode 100644 index 8a78e92f9..000000000 --- a/test/session/with-jwt/jwtFunctions.test.js +++ /dev/null @@ -1,169 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -let { ProcessState } = require("../../../lib/build/processState"); -let SuperTokens = require("../../../"); -let Session = require("../../../recipe/session"); -let { Querier } = require("../../../lib/build/querier"); -let { maxVersion } = require("../../../lib/build/utils"); - -describe(`session-jwt-functions: ${printPath("[test/session/with-jwt/jwtFunctions.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that JWT functions fail if the jwt feature is not enabled", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - try { - await Session.createJWT({}); - throw new Error("createJWT succeeded when it should have failed"); - } catch (e) { - if ( - e.message !== - "createJWT cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ) { - throw e; - } - } - - try { - await Session.getJWKS(); - throw new Error("getJWKS succeeded when it should have failed"); - } catch (e) { - if ( - e.message !== - "getJWKS cannot be used without enabling the JWT feature. Please set 'enableJWT: true' when initialising the Session recipe" - ) { - throw e; - } - } - }); - - it("Test that JWT functions work if the jwt feature is enabled", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - await Session.createJWT({}); - await Session.getJWKS(); - }); -}); diff --git a/test/session/with-jwt/jwtFunctions.test.ts b/test/session/with-jwt/jwtFunctions.test.ts new file mode 100644 index 000000000..8992819d6 --- /dev/null +++ b/test/session/with-jwt/jwtFunctions.test.ts @@ -0,0 +1,157 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +describe(`session-jwt-functions: ${printPath('[test/session/with-jwt/jwtFunctions.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that JWT functions fail if the jwt feature is not enabled', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ req, res, userId, accessTokenPayload, sessionData }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ req, res, userId, accessTokenPayload, sessionData }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + try { + await Session.createJWT({}) + throw new Error('createJWT succeeded when it should have failed') + } + catch (e: any) { + if ( + e.message + !== 'createJWT cannot be used without enabling the JWT feature. Please set \'enableJWT: true\' when initialising the Session recipe' + ) + throw e + } + + try { + await Session.getJWKS() + throw new Error('getJWKS succeeded when it should have failed') + } + catch (e: any) { + if ( + e.message + !== 'getJWKS cannot be used without enabling the JWT feature. Please set \'enableJWT: true\' when initialising the Session recipe' + ) + throw e + } + }) + + it('Test that JWT functions work if the jwt feature is enabled', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + await Session.createJWT({}) + await Session.getJWKS() + }) +}) diff --git a/test/session/with-jwt/session.override.test.js b/test/session/with-jwt/session.override.test.js deleted file mode 100644 index a414c1724..000000000 --- a/test/session/with-jwt/session.override.test.js +++ /dev/null @@ -1,689 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, -} = require("../../utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState, PROCESS_STATE } = require("../../../lib/build/processState"); -let SuperTokens = require("../../../"); -let Session = require("../../../recipe/session"); -let { Querier } = require("../../../lib/build/querier"); -let { maxVersion } = require("../../../lib/build/utils"); -const { verifySession } = require("../../../recipe/session/framework/express"); -let { middleware, errorHandler } = require("../../../framework/express"); - -/** - * Test that overriding the session recipe functions and apis still work when the JWT feature is enabled - */ -describe(`session: ${printPath("[test/session/with-jwt/session.override.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test overriding of sessions functions", async function () { - await startST(); - - let createNewSessionCalled = false; - let getSessionCalled = false; - let refreshSessionCalled = false; - let session = undefined; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - jwt: { enable: true }, - override: { - functions: function (oI) { - return { - ...oI, - createNewSession: async function (input) { - let response = await oI.createNewSession(input); - createNewSessionCalled = true; - session = response; - return response; - }, - getSession: async function (input) { - let response = await oI.getSession(input); - getSessionCalled = true; - session = response; - return response; - }, - refreshSession: async function (input) { - let response = await oI.refreshSession(input); - refreshSessionCalled = true; - session = response; - return response; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.post("/session/revoke", verifySession(), async (req, res) => { - let session = req.session; - await session.revokeSession(); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(createNewSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert(res.accessToken !== undefined); - assert.strictEqual(session.getAccessToken(), decodeURIComponent(res.accessToken)); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - session = undefined; - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(refreshSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert(res2.accessToken !== undefined); - assert.strictEqual(session.getAccessToken(), decodeURIComponent(res2.accessToken)); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - session = undefined; - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert.strictEqual(getSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - assert.strictEqual(session.getAccessToken(), decodeURIComponent(res3.accessToken)); - - ProcessState.getInstance().reset(); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test overriding of sessions functions, error thrown", async function () { - await startST(); - - let createNewSessionCalled = false; - let session = undefined; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - jwt: { enable: true }, - override: { - functions: function (oI) { - return { - ...oI, - createNewSession: async function (input) { - let response = await oI.createNewSession(input); - createNewSessionCalled = true; - session = response; - throw { - error: "create new session error", - }; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res, next) => { - try { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - } catch (err) { - next(err); - } - }); - - app.use(errorHandler()); - - app.use((err, req, res, next) => { - res.json({ - customError: true, - ...err, - }); - }); - - let res = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.strictEqual(createNewSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert.deepStrictEqual(res, { customError: true, error: "create new session error" }); - }); - - it("test overriding of sessions apis", async function () { - await startST(); - - let signoutCalled = false; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - jwt: { enable: true }, - override: { - apis: function (oI) { - return { - ...oI, - signOutPOST: async function (input) { - let response = await oI.signOutPOST(input); - signoutCalled = true; - return response; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert.strictEqual(signoutCalled, true); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test overriding of sessions apis, error thrown", async function () { - await startST(); - - let signoutCalled = false; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - jwt: { enable: true }, - override: { - apis: function (oI) { - return { - ...oI, - signOutPOST: async function (input) { - let response = await oI.signOutPOST(input); - signoutCalled = true; - throw { - error: "signout error", - }; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.use(errorHandler()); - - app.use((err, req, res, next) => { - res.json({ - customError: true, - ...err, - }); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.strictEqual(signoutCalled, true); - assert.deepStrictEqual(sessionRevokedResponse, { customError: true, error: "signout error" }); - }); - - it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - apis: function (oI) { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.status === 404); - }); - - it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - apis: function (oI) { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.status === 404); - }); -}); diff --git a/test/session/with-jwt/session.override.test.ts b/test/session/with-jwt/session.override.test.ts new file mode 100644 index 000000000..0608a5e82 --- /dev/null +++ b/test/session/with-jwt/session.override.test.ts @@ -0,0 +1,683 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import express from 'express' +import request from 'supertest' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { verifySession } from 'supertokens-node/recipe/session/framework/express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractInfoFromResponse, + killAllST, + printPath, + setupST, + startST, +} from '../../utils' +/** + * Test that overriding the session recipe functions and apis still work when the JWT feature is enabled + */ +describe(`session: ${printPath('[test/session/with-jwt/session.override.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test overriding of sessions functions', async () => { + await startST() + + let createNewSessionCalled = false + let getSessionCalled = false + let refreshSessionCalled = false + let session + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + jwt: { enable: true }, + override: { + functions(oI) { + return { + ...oI, + async createNewSession(input) { + const response = await oI.createNewSession(input) + createNewSessionCalled = true + session = response + return response + }, + async getSession(input) { + const response = await oI.getSession(input) + getSessionCalled = true + session = response + return response + }, + async refreshSession(input) { + const response = await oI.refreshSession(input) + refreshSessionCalled = true + session = response + return response + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.post('/session/revoke', verifySession(), async (req, res) => { + const session = req.session + await session.revokeSession() + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(createNewSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert(res.accessToken !== undefined) + assert.strictEqual(session.getAccessToken(), decodeURIComponent(res.accessToken)) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + session = undefined + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(refreshSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert(res2.accessToken !== undefined) + assert.strictEqual(session.getAccessToken(), decodeURIComponent(res2.accessToken)) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + session = undefined + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert.strictEqual(getSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + assert.strictEqual(session.getAccessToken(), decodeURIComponent(res3.accessToken)) + + ProcessState.getInstance().reset() + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test overriding of sessions functions, error thrown', async () => { + await startST() + + let createNewSessionCalled = false + let session + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + jwt: { enable: true }, + override: { + functions(oI) { + return { + ...oI, + async createNewSession(input) { + const response = await oI.createNewSession(input) + createNewSessionCalled = true + session = response + throw { + error: 'create new session error', + } + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res, next) => { + try { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + } + catch (err) { + next(err) + } + }) + + app.use(errorHandler()) + + app.use((err, req, res, next) => { + res.json({ + customError: true, + ...err, + }) + }) + + const res = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.strictEqual(createNewSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert.deepStrictEqual(res, { customError: true, error: 'create new session error' }) + }) + + it('test overriding of sessions apis', async () => { + await startST() + + let signoutCalled = false + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + jwt: { enable: true }, + override: { + apis(oI) { + return { + ...oI, + async signOutPOST(input) { + const response = await oI.signOutPOST(input) + signoutCalled = true + return response + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert.strictEqual(signoutCalled, true) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test overriding of sessions apis, error thrown', async () => { + await startST() + + let signoutCalled = false + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + jwt: { enable: true }, + override: { + apis(oI) { + return { + ...oI, + async signOutPOST(input) { + const response = await oI.signOutPOST(input) + signoutCalled = true + throw { + error: 'signout error', + } + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.use(errorHandler()) + + app.use((err, req, res, next) => { + res.json({ + customError: true, + ...err, + }) + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.strictEqual(signoutCalled, true) + assert.deepStrictEqual(sessionRevokedResponse, { customError: true, error: 'signout error' }) + }) + + it('test that if disabling api, the default refresh API does not work', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + apis(oI) { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.status === 404) + }) + + it('test that if disabling api, the default sign out API does not work', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + apis(oI) { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/signout') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.status === 404) + }) +}) diff --git a/test/session/with-jwt/sessionClass.test.js b/test/session/with-jwt/sessionClass.test.js deleted file mode 100644 index 6fb3e2fb6..000000000 --- a/test/session/with-jwt/sessionClass.test.js +++ /dev/null @@ -1,558 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const JsonWebToken = require("jsonwebtoken"); - -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse, resetAll } = require("../../utils"); -let { Querier } = require("../../../lib/build/querier"); -let { maxVersion } = require("../../../lib/build/utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState } = require("../../../lib/build/processState"); -let SuperTokens = require("../../../"); -let Session = require("../../../recipe/session"); -let { middleware, errorHandler } = require("../../../framework/express"); -const { TrueClaim } = require("../claims/testClaims"); - -describe(`session-jwt-functions: ${printPath("[test/session/with-jwt/sessionClass.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that updating access token payload works", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - - await session.updateAccessTokenPayload({ newKey: "newValue" }); - - res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let accessTokenPayload = createJWTResponse.body.accessTokenPayload; - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.newKey, "newValue"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - assert.strictEqual(decodedJWT.newKey, "newValue"); - }); - - it("Test that updating access token payload by mergeIntoAccessTokenPayload works", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function (input) { - const accessTokenPayload = { - ...input.accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ ...input, accessTokenPayload }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - - await session.mergeIntoAccessTokenPayload({ newKey: "newValue" }); - - res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let accessTokenPayload = createJWTResponse.body.accessTokenPayload; - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.newKey, "newValue"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - assert.strictEqual(decodedJWT.newKey, "newValue"); - }); - - it("Test that both access token payload and JWT have valid claims when calling update with a undefined payload", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function (input) { - const accessTokenPayload = { - ...input.accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ ...input, accessTokenPayload }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - - await session.updateAccessTokenPayload(undefined); - - res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let accessTokenPayload = createJWTResponse.body.accessTokenPayload; - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.strictEqual(accessTokenPayload.customClaim, undefined); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - assert.strictEqual(decodedJWT.customClaim, undefined); - }); - - it("should update JWT when setting claim value by fetchAndSetClaim", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function (input) { - const accessTokenPayload = { - ...input.accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ ...input, accessTokenPayload }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - - await session.fetchAndSetClaim(TrueClaim); - - res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let accessTokenPayload = createJWTResponse.body.accessTokenPayload; - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), true); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), true); - }); - - it("should update JWT when setting claim value by setClaimValue", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function (input) { - const accessTokenPayload = { - ...input.accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ ...input, accessTokenPayload }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - - await session.setClaimValue(TrueClaim, false); - - res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let accessTokenPayload = createJWTResponse.body.accessTokenPayload; - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), false); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), false); - }); - - it("should update JWT when removing claim", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function (input) { - const accessTokenPayload = { - ...input.accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ ...input, accessTokenPayload }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - - await session.setClaimValue(TrueClaim, true); - await session.removeClaim(TrueClaim); - - res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let accessTokenPayload = createJWTResponse.body.accessTokenPayload; - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), undefined); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), undefined); - }); -}); diff --git a/test/session/with-jwt/sessionClass.test.ts b/test/session/with-jwt/sessionClass.test.ts new file mode 100644 index 000000000..2fd336c7e --- /dev/null +++ b/test/session/with-jwt/sessionClass.test.ts @@ -0,0 +1,553 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import JsonWebToken from 'jsonwebtoken' +import express from 'express' +import request from 'supertest' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' +import { TrueClaim } from '../claims/testClaims' + +describe(`session-jwt-functions: ${printPath('[test/session/with-jwt/sessionClass.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that updating access token payload works', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + + await session.updateAccessTokenPayload({ newKey: 'newValue' }) + + res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const accessTokenPayload = createJWTResponse.body.accessTokenPayload + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.newKey, 'newValue') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + assert.strictEqual(decodedJWT.newKey, 'newValue') + }) + + it('Test that updating access token payload by mergeIntoAccessTokenPayload works', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession(input) { + const accessTokenPayload = { + ...input.accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ ...input, accessTokenPayload }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + + await session.mergeIntoAccessTokenPayload({ newKey: 'newValue' }) + + res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const accessTokenPayload = createJWTResponse.body.accessTokenPayload + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.newKey, 'newValue') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + assert.strictEqual(decodedJWT.newKey, 'newValue') + }) + + it('Test that both access token payload and JWT have valid claims when calling update with a undefined payload', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession(input) { + const accessTokenPayload = { + ...input.accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ ...input, accessTokenPayload }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + + await session.updateAccessTokenPayload(undefined) + + res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const accessTokenPayload = createJWTResponse.body.accessTokenPayload + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.strictEqual(accessTokenPayload.customClaim, undefined) + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + assert.strictEqual(decodedJWT.customClaim, undefined) + }) + + it('should update JWT when setting claim value by fetchAndSetClaim', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession(input) { + const accessTokenPayload = { + ...input.accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ ...input, accessTokenPayload }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + + await session.fetchAndSetClaim(TrueClaim) + + res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const accessTokenPayload = createJWTResponse.body.accessTokenPayload + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), true) + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), true) + }) + + it('should update JWT when setting claim value by setClaimValue', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession(input) { + const accessTokenPayload = { + ...input.accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ ...input, accessTokenPayload }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + + await session.setClaimValue(TrueClaim, false) + + res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const accessTokenPayload = createJWTResponse.body.accessTokenPayload + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), false) + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), false) + }) + + it('should update JWT when removing claim', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession(input) { + const accessTokenPayload = { + ...input.accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ ...input, accessTokenPayload }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + + await session.setClaimValue(TrueClaim, true) + await session.removeClaim(TrueClaim) + + res.status(200).json({ accessTokenPayload: session.getAccessTokenPayload() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const accessTokenPayload = createJWTResponse.body.accessTokenPayload + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(TrueClaim.getValueFromPayload(accessTokenPayload), undefined) + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + assert.strictEqual(TrueClaim.getValueFromPayload(decodedJWT), undefined) + }) +}) diff --git a/test/session/with-jwt/withjwt.test.js b/test/session/with-jwt/withjwt.test.js deleted file mode 100644 index e7d045e6d..000000000 --- a/test/session/with-jwt/withjwt.test.js +++ /dev/null @@ -1,2334 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const JsonWebToken = require("jsonwebtoken"); - -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - resetAll, - setKeyValueInConfig, - delay, -} = require("../../utils"); -let { Querier } = require("../../../lib/build/querier"); -let { maxVersion } = require("../../../lib/build/utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState } = require("../../../lib/build/processState"); -let SuperTokens = require("../../../"); -let Session = require("../../../recipe/session"); -let { middleware, errorHandler } = require("../../../framework/express"); -let { - setJWTExpiryOffsetSecondsForTesting, -} = require("../../../lib/build/recipe/session/with-jwt/recipeImplementation"); - -describe(`session-with-jwt: ${printPath("[test/session/with-jwt/withjwt.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("Test that when creating a session with a custom access token payload, the payload has a jwt in it and the jwt has the user defined payload keys", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - let sessionHandle = createJWTResponse.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let accessTokenPayloadJWT = accessTokenPayload.jwt; - - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(accessTokenPayloadJWT, undefined); - - let decodedJWTPayload = JsonWebToken.decode(accessTokenPayloadJWT); - - assert(decodedJWTPayload.customKey === "customValue"); - assert(decodedJWTPayload.customKey2 === "customValue2"); - }); - - it("Test that when creating a session the JWT expiry is 30 seconds more than the access token expiry", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let responseInfo = extractInfoFromResponse(createJWTResponse); - - let accessTokenExpiryInSeconds = - JSON.parse( - Buffer.from(decodeURIComponent(responseInfo.accessToken).split(".")[1], "base64").toString("utf-8") - ).expiryTime / 1000; - let sessionHandle = createJWTResponse.body.sessionHandle; - let sessionInformation = await Session.getSessionInformation(sessionHandle); - - let jwtPayload = sessionInformation.accessTokenPayload.jwt.split(".")[1]; - let jwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, "base64").toString("utf-8")).exp; - let expiryDiff = jwtExpiryInSeconds - accessTokenExpiryInSeconds; - - // We check that JWT expiry is 30 seconds more than access token expiry. Accounting for a 5s skew - assert(27 <= expiryDiff); - assert(expiryDiff <= 32); - }); - - it("Test that when a session is refreshed, the JWT expiry is updated correctly", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.get("/getSession", async (req, res) => { - let session = await Session.getSession(req, res); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let responseInfo = extractInfoFromResponse(createJWTResponse); - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - let jwtPayload = accessTokenPayload.jwt.split(".")[1]; - let jwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, "base64").toString("utf-8")).exp; - - let delay = 5; - await new Promise((res) => { - setTimeout(() => { - res(); - }, delay * 1000); - }); - - let refreshResponse = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + responseInfo.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - responseInfo = extractInfoFromResponse(refreshResponse); - accessTokenExpiryInSeconds = new Date(responseInfo.accessTokenExpiry).getTime() / 1000; - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - jwtPayload = accessTokenPayload.jwt.split(".")[1]; - let newJWTExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, "base64").toString("utf-8")).exp; - - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - // Make sure that the new expiry is greater than the old one by the amount of delay before refresh, accounting for a second skew - assert( - newJWTExpiryInSeconds - jwtExpiryInSeconds === delay || - newJWTExpiryInSeconds - jwtExpiryInSeconds === delay + 1 - ); - }); - - it("Test that mergeIntoAccessTokenPayload updates JWT", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function (input) { - const accessTokenPayload = { - ...input.accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ ...input, accessTokenPayload }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - let jwtPayload = accessTokenPayload.jwt.split(".")[1]; - jwtPayload = accessTokenPayload.jwt.split(".")[1]; - let parsedJWTPayload = JSON.parse(Buffer.from(jwtPayload, "base64").toString("utf-8")); - assert.strictEqual(parsedJWTPayload.newKey, undefined); - let jwtExpiryInSeconds = parsedJWTPayload.exp; - - await Session.mergeIntoAccessTokenPayload(sessionHandle, { newKey: "newValue" }); - - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - jwtPayload = accessTokenPayload.jwt.split(".")[1]; - parsedJWTPayload = JSON.parse(Buffer.from(jwtPayload, "base64").toString("utf-8")); - assert.strictEqual(parsedJWTPayload.newKey, "newValue"); - - const newJwtExpiryInSeconds = parsedJWTPayload.exp; - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert(jwtExpiryInSeconds + 100 >= newJwtExpiryInSeconds && jwtExpiryInSeconds - 100 <= newJwtExpiryInSeconds); - }); - - it("Test that when updating access token payload, jwt expiry does not change", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.get("/getSession", async (req, res) => { - let session = await Session.getSession(req, res); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - let jwtPayload = accessTokenPayload.jwt.split(".")[1]; - let jwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, "base64").toString("utf-8")).exp; - - await Session.updateAccessTokenPayload(sessionHandle, { newKey: "newValue" }); - - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - jwtPayload = accessTokenPayload.jwt.split(".")[1]; - let newJwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, "base64").toString("utf-8")).exp; - - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert(jwtExpiryInSeconds + 100 >= newJwtExpiryInSeconds && jwtExpiryInSeconds - 100 <= newJwtExpiryInSeconds); - }); - - it("Test that for sessions created without jwt enabled, calling updateAccessTokenPayload after enabling jwt does not create a jwt", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.get("/getSession", async (req, res) => { - let session = await Session.getSession(req, res); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.strictEqual(accessTokenPayload.jwt, undefined); - - resetAll(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - await Session.updateAccessTokenPayload(sessionHandle, { someKey: "someValue" }); - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, undefined); - assert.strictEqual(accessTokenPayload.jwt, undefined); - assert.equal(accessTokenPayload.someKey, "someValue"); - }); - - it("Test that for sessions created without jwt enabled, refreshing session after enabling jwt adds a JWT to the access token payload", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.get("/getSession", async (req, res) => { - let session = await Session.getSession(req, res); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let responseInfo = extractInfoFromResponse(createJWTResponse); - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.strictEqual(accessTokenPayload.jwt, undefined); - - resetAll(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + responseInfo.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - }); - - it("Test that when creating a session with jwt enabled, the sub claim gets added", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - }); - - it("Test that when creating a session with jwt enabled and using a custom sub claim, the custom claim value gets used", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - sub: "customsub", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "customsub"); - assert.strictEqual(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - }); - - it("Test that when creating a session with jwt enabled, the iss claim gets added", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customKey: "customValue", - customKey2: "customValue2", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["iss"], "https://api.supertokens.io/auth"); - }); - - it("Test that when creating a session with jwt enabled and using a custom iss claim, the custom claim value gets used", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - iss: "customIss", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["iss"], "customIss"); - }); - - it("Test that sub and iss claims are still present after calling updateAccessTokenPayload", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT.customClaim, "customValue"); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - - await Session.updateAccessTokenPayload(sessionHandle, { newCustomClaim: "newValue" }); - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - - decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT.newCustomClaim, "newValue"); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - }); - - it("Test that sub and iss claims are still present after refreshing the session", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let responseInfo = extractInfoFromResponse(createJWTResponse); - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT.customClaim, "customValue"); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + responseInfo.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - - decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - }); - - it("Test that enabling JWT with a custom property name works as expected", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true, propertyNameInAccessTokenPayload: "customPropertyName" }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "customPropertyName"); - assert.strictEqual(accessTokenPayload.jwt, undefined); - assert.notStrictEqual(accessTokenPayload.customPropertyName, undefined); - }); - - it("Test that the JWT payload is maintained after updating the access token payload and refreshing the session", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.post("/refreshsession", async (req, res) => { - let newSession = await Session.refreshSession(req, res); - res.status(200).json({ accessTokenPayload: newSession.getAccessTokenPayload() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let responseInfo = extractInfoFromResponse(createJWTResponse); - await Session.updateAccessTokenPayload(sessionHandle, { newClaim: "newValue" }); - - let refreshResponse = await new Promise((resolve) => - request(app) - .post("/refreshsession") - .set("Cookie", ["sRefreshToken=" + responseInfo.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.equal(refreshResponse.body.accessTokenPayload.sub, undefined); - assert.equal(refreshResponse.body.accessTokenPayload.iss, undefined); - assert.strictEqual(refreshResponse.body.accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(refreshResponse.body.accessTokenPayload, undefined); - assert.strictEqual(refreshResponse.body.accessTokenPayload.newClaim, "newValue"); - }); - - it("Test that access token payload has valid properties when creating, updating and refreshing", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let responseInfo = extractInfoFromResponse(createJWTResponse); - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.strictEqual(accessTokenPayload.customClaim, "customValue"); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - - await Session.updateAccessTokenPayload(sessionHandle, { newKey: "newValue" }); - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.strictEqual(accessTokenPayload.customClaim, undefined); - assert.strictEqual(accessTokenPayload.newKey, "newValue"); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + responseInfo.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - assert.strictEqual(accessTokenPayload.newKey, "newValue"); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload.sub, undefined); - assert.strictEqual(accessTokenPayload.iss, undefined); - }); - - it("Test that after changing the jwt property name, updating access token payload does not change the _jwtPName", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - - resetAll(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true, propertyNameInAccessTokenPayload: "jwtProperty" }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - await Session.updateAccessTokenPayload(sessionHandle, { newKey: "newValue" }); - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.newKey, "newValue"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload.jwtProperty, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - }); - - it("Test that after changing the jwt property name, refreshing the session changes the _jwtPName", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let responseInfo = extractInfoFromResponse(createJWTResponse); - - resetAll(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true, propertyNameInAccessTokenPayload: "jwtProperty" }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + responseInfo.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.customClaim, "customValue"); - assert.strictEqual(accessTokenPayload.jwt, undefined); - assert.notStrictEqual(accessTokenPayload.jwtProperty, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwtProperty"); - }); - - it("Test that after access token expiry and JWT expiry and refreshing the session, the access token payload and JWT are valid", async function () { - await setKeyValueInConfig("access_token_validity", 2); - await startST(); - setJWTExpiryOffsetSecondsForTesting(2); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - let responseInfo = extractInfoFromResponse(createJWTResponse); - - await delay(5); - - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - - let currentTimeInSeconds = Math.ceil(Date.now() / 1000); - // Make sure that the JWT has expired - assert(decodedJWT.exp < currentTimeInSeconds); - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.customClaim, "customValue"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + responseInfo.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.customClaim, "customValue"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - // Make sure the JWT is not expired after refreshing - decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - currentTimeInSeconds = Math.ceil(Date.now() / 1000); - assert(decodedJWT.exp > currentTimeInSeconds); - }); - - it("Test that both access token payload and JWT have valid claims when calling update with a undefined payload", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - jwt: { enable: true }, - override: { - functions: function (oi) { - return { - ...oi, - createNewSession: async function ({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }) { - accessTokenPayload = { - ...accessTokenPayload, - customClaim: "customValue", - }; - - return await oi.createNewSession({ - req, - res, - userId, - accessTokenPayload, - sessionData, - }); - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", {}, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.customClaim, "customValue"); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - - await Session.updateAccessTokenPayload(sessionHandle, undefined); - - accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.strictEqual(accessTokenPayload.customClaim, undefined); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - }); - - it("Test that both access token payload and JWT have valid claims when creating a session with an undefined payload", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", jwt: { enable: true } })], - }); - - // Only run for version >= 2.9 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.8") === "2.8") { - return; - } - - let app = express(); - - app.use(middleware()); - app.use(express.json()); - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "userId", undefined, {}); - res.status(200).json({ sessionHandle: session.getHandle() }); - }); - - app.use(errorHandler()); - - let createJWTResponse = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionHandle = createJWTResponse.body.sessionHandle; - - let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload; - assert.equal(accessTokenPayload.sub, undefined); - assert.equal(accessTokenPayload.iss, undefined); - assert.notStrictEqual(accessTokenPayload.jwt, undefined); - assert.strictEqual(accessTokenPayload._jwtPName, "jwt"); - - let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt); - assert.notStrictEqual(decodedJWT, null); - assert.strictEqual(decodedJWT["sub"], "userId"); - assert.strictEqual(decodedJWT.iss, "https://api.supertokens.io/auth"); - assert.strictEqual(decodedJWT._jwtPName, undefined); - }); -}); diff --git a/test/session/with-jwt/withjwt.test.ts b/test/session/with-jwt/withjwt.test.ts new file mode 100644 index 000000000..8e92eb5a1 --- /dev/null +++ b/test/session/with-jwt/withjwt.test.ts @@ -0,0 +1,2312 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import JsonWebToken from 'jsonwebtoken' +import express from 'express' +import request from 'supertest' +import { ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { setJWTExpiryOffsetSecondsForTesting } from 'supertokens-node/recipe/session/with-jwt/recipeImplementation' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + delay, + extractInfoFromResponse, + killAllST, + printPath, + resetAll, + setKeyValueInConfig, + setupST, + startST, +} from '../../utils' + +describe(`session-with-jwt: ${printPath('[test/session/with-jwt/withjwt.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('Test that when creating a session with a custom access token payload, the payload has a jwt in it and the jwt has the user defined payload keys', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + const sessionHandle = createJWTResponse.sessionHandle + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + const accessTokenPayloadJWT = accessTokenPayload.jwt + + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(accessTokenPayloadJWT, undefined) + + const decodedJWTPayload = JsonWebToken.decode(accessTokenPayloadJWT) + + assert(decodedJWTPayload.customKey === 'customValue') + assert(decodedJWTPayload.customKey2 === 'customValue2') + }) + + it('Test that when creating a session the JWT expiry is 30 seconds more than the access token expiry', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, '', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const responseInfo = extractInfoFromResponse(createJWTResponse) + + const accessTokenExpiryInSeconds + = JSON.parse( + Buffer.from(decodeURIComponent(responseInfo.accessToken).split('.')[1], 'base64').toString('utf-8'), + ).expiryTime / 1000 + const sessionHandle = createJWTResponse.body.sessionHandle + const sessionInformation = await Session.getSessionInformation(sessionHandle) + + const jwtPayload = sessionInformation.accessTokenPayload.jwt.split('.')[1] + const jwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf-8')).exp + const expiryDiff = jwtExpiryInSeconds - accessTokenExpiryInSeconds + + // We check that JWT expiry is 30 seconds more than access token expiry. Accounting for a 5s skew + assert(expiryDiff >= 27) + assert(expiryDiff <= 32) + }) + + it('Test that when a session is refreshed, the JWT expiry is updated correctly', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.get('/getSession', async (req, res) => { + const session = await Session.getSession(req, res) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + let responseInfo = extractInfoFromResponse(createJWTResponse) + const sessionHandle = createJWTResponse.body.sessionHandle + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + let jwtPayload = accessTokenPayload.jwt.split('.')[1] + const jwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf-8')).exp + + const delay = 5 + await new Promise((res) => { + setTimeout(() => { + res() + }, delay * 1000) + }) + + const refreshResponse = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${responseInfo.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + responseInfo = extractInfoFromResponse(refreshResponse) + const accessTokenExpiryInSeconds = new Date(responseInfo.accessTokenExpiry).getTime() / 1000 + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + jwtPayload = accessTokenPayload.jwt.split('.')[1] + const newJWTExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf-8')).exp + + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + // Make sure that the new expiry is greater than the old one by the amount of delay before refresh, accounting for a second skew + assert( + newJWTExpiryInSeconds - jwtExpiryInSeconds === delay + || newJWTExpiryInSeconds - jwtExpiryInSeconds === delay + 1, + ) + }) + + it('Test that mergeIntoAccessTokenPayload updates JWT', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession(input) { + const accessTokenPayload = { + ...input.accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ ...input, accessTokenPayload }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + let jwtPayload = accessTokenPayload.jwt.split('.')[1] + jwtPayload = accessTokenPayload.jwt.split('.')[1] + let parsedJWTPayload = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf-8')) + assert.strictEqual(parsedJWTPayload.newKey, undefined) + const jwtExpiryInSeconds = parsedJWTPayload.exp + + await Session.mergeIntoAccessTokenPayload(sessionHandle, { newKey: 'newValue' }) + + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + jwtPayload = accessTokenPayload.jwt.split('.')[1] + parsedJWTPayload = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf-8')) + assert.strictEqual(parsedJWTPayload.newKey, 'newValue') + + const newJwtExpiryInSeconds = parsedJWTPayload.exp + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert(jwtExpiryInSeconds + 100 >= newJwtExpiryInSeconds && jwtExpiryInSeconds - 100 <= newJwtExpiryInSeconds) + }) + + it('Test that when updating access token payload, jwt expiry does not change', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.get('/getSession', async (req, res) => { + const session = await Session.getSession(req, res) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + let jwtPayload = accessTokenPayload.jwt.split('.')[1] + const jwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf-8')).exp + + await Session.updateAccessTokenPayload(sessionHandle, { newKey: 'newValue' }) + + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + jwtPayload = accessTokenPayload.jwt.split('.')[1] + const newJwtExpiryInSeconds = JSON.parse(Buffer.from(jwtPayload, 'base64').toString('utf-8')).exp + + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert(jwtExpiryInSeconds + 100 >= newJwtExpiryInSeconds && jwtExpiryInSeconds - 100 <= newJwtExpiryInSeconds) + }) + + it('Test that for sessions created without jwt enabled, calling updateAccessTokenPayload after enabling jwt does not create a jwt', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, '', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.get('/getSession', async (req, res) => { + const session = await Session.getSession(req, res) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.strictEqual(accessTokenPayload.jwt, undefined) + + resetAll() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + await Session.updateAccessTokenPayload(sessionHandle, { someKey: 'someValue' }) + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, undefined) + assert.strictEqual(accessTokenPayload.jwt, undefined) + assert.equal(accessTokenPayload.someKey, 'someValue') + }) + + it('Test that for sessions created without jwt enabled, refreshing session after enabling jwt adds a JWT to the access token payload', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.get('/getSession', async (req, res) => { + const session = await Session.getSession(req, res) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const responseInfo = extractInfoFromResponse(createJWTResponse) + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.strictEqual(accessTokenPayload.jwt, undefined) + + resetAll() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${responseInfo.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + }) + + it('Test that when creating a session with jwt enabled, the sub claim gets added', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + }) + + it('Test that when creating a session with jwt enabled and using a custom sub claim, the custom claim value gets used', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + sub: 'customsub', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'customsub') + assert.strictEqual(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + }) + + it('Test that when creating a session with jwt enabled, the iss claim gets added', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customKey: 'customValue', + customKey2: 'customValue2', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + }) + + it('Test that when creating a session with jwt enabled and using a custom iss claim, the custom claim value gets used', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + iss: 'customIss', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.iss, 'customIss') + }) + + it('Test that sub and iss claims are still present after calling updateAccessTokenPayload', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.customClaim, 'customValue') + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + + await Session.updateAccessTokenPayload(sessionHandle, { newCustomClaim: 'newValue' }) + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + + decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.newCustomClaim, 'newValue') + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + }) + + it('Test that sub and iss claims are still present after refreshing the session', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const responseInfo = extractInfoFromResponse(createJWTResponse) + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.customClaim, 'customValue') + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${responseInfo.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + + decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + }) + + it('Test that enabling JWT with a custom property name works as expected', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true, propertyNameInAccessTokenPayload: 'customPropertyName' }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'customPropertyName') + assert.strictEqual(accessTokenPayload.jwt, undefined) + assert.notStrictEqual(accessTokenPayload.customPropertyName, undefined) + }) + + it('Test that the JWT payload is maintained after updating the access token payload and refreshing the session', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.post('/refreshsession', async (req, res) => { + const newSession = await Session.refreshSession(req, res) + res.status(200).json({ accessTokenPayload: newSession.getAccessTokenPayload() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const responseInfo = extractInfoFromResponse(createJWTResponse) + await Session.updateAccessTokenPayload(sessionHandle, { newClaim: 'newValue' }) + + const refreshResponse = await new Promise(resolve => + request(app) + .post('/refreshsession') + .set('Cookie', [`sRefreshToken=${responseInfo.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.equal(refreshResponse.body.accessTokenPayload.sub, undefined) + assert.equal(refreshResponse.body.accessTokenPayload.iss, undefined) + assert.strictEqual(refreshResponse.body.accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(refreshResponse.body.accessTokenPayload, undefined) + assert.strictEqual(refreshResponse.body.accessTokenPayload.newClaim, 'newValue') + }) + + it('Test that access token payload has valid properties when creating, updating and refreshing', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const responseInfo = extractInfoFromResponse(createJWTResponse) + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.strictEqual(accessTokenPayload.customClaim, 'customValue') + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + + await Session.updateAccessTokenPayload(sessionHandle, { newKey: 'newValue' }) + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.strictEqual(accessTokenPayload.customClaim, undefined) + assert.strictEqual(accessTokenPayload.newKey, 'newValue') + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${responseInfo.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + assert.strictEqual(accessTokenPayload.newKey, 'newValue') + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload.sub, undefined) + assert.strictEqual(accessTokenPayload.iss, undefined) + }) + + it('Test that after changing the jwt property name, updating access token payload does not change the _jwtPName', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + + resetAll() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true, propertyNameInAccessTokenPayload: 'jwtProperty' }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + await Session.updateAccessTokenPayload(sessionHandle, { newKey: 'newValue' }) + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.newKey, 'newValue') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload.jwtProperty, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + }) + + it('Test that after changing the jwt property name, refreshing the session changes the _jwtPName', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const responseInfo = extractInfoFromResponse(createJWTResponse) + + resetAll() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true, propertyNameInAccessTokenPayload: 'jwtProperty' }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${responseInfo.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.customClaim, 'customValue') + assert.strictEqual(accessTokenPayload.jwt, undefined) + assert.notStrictEqual(accessTokenPayload.jwtProperty, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwtProperty') + }) + + it('Test that after access token expiry and JWT expiry and refreshing the session, the access token payload and JWT are valid', async () => { + await setKeyValueInConfig('access_token_validity', 2) + await startST() + setJWTExpiryOffsetSecondsForTesting(2) + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + const responseInfo = extractInfoFromResponse(createJWTResponse) + + await delay(5) + + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + + let currentTimeInSeconds = Math.ceil(Date.now() / 1000) + // Make sure that the JWT has expired + assert(decodedJWT.exp < currentTimeInSeconds) + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.customClaim, 'customValue') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${responseInfo.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.customClaim, 'customValue') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + // Make sure the JWT is not expired after refreshing + decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + currentTimeInSeconds = Math.ceil(Date.now() / 1000) + assert(decodedJWT.exp > currentTimeInSeconds) + }) + + it('Test that both access token payload and JWT have valid claims when calling update with a undefined payload', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + jwt: { enable: true }, + override: { + functions(oi) { + return { + ...oi, + async createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) { + accessTokenPayload = { + ...accessTokenPayload, + customClaim: 'customValue', + } + + return await oi.createNewSession({ + req, + res, + userId, + accessTokenPayload, + sessionData, + }) + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', {}, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + + let accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.customClaim, 'customValue') + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + let decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + + await Session.updateAccessTokenPayload(sessionHandle, undefined) + + accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.strictEqual(accessTokenPayload.customClaim, undefined) + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + }) + + it('Test that both access token payload and JWT have valid claims when creating a session with an undefined payload', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', jwt: { enable: true } })], + }) + + // Only run for version >= 2.9 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.8') === '2.8') + return + + const app = express() + + app.use(middleware()) + app.use(express.json()) + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, 'userId', undefined, {}) + res.status(200).json({ sessionHandle: session.getHandle() }) + }) + + app.use(errorHandler()) + + const createJWTResponse = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionHandle = createJWTResponse.body.sessionHandle + + const accessTokenPayload = (await Session.getSessionInformation(sessionHandle)).accessTokenPayload + assert.equal(accessTokenPayload.sub, undefined) + assert.equal(accessTokenPayload.iss, undefined) + assert.notStrictEqual(accessTokenPayload.jwt, undefined) + assert.strictEqual(accessTokenPayload._jwtPName, 'jwt') + + const decodedJWT = JsonWebToken.decode(accessTokenPayload.jwt) + assert.notStrictEqual(decodedJWT, null) + assert.strictEqual(decodedJWT.sub, 'userId') + assert.strictEqual(decodedJWT.iss, 'https://api.supertokens.io/auth') + assert.strictEqual(decodedJWT._jwtPName, undefined) + }) +}) diff --git a/test/sessionAccessTokenSigningKeyUpdate.test.js b/test/sessionAccessTokenSigningKeyUpdate.test.js deleted file mode 100644 index 4ac04e95b..000000000 --- a/test/sessionAccessTokenSigningKeyUpdate.test.js +++ /dev/null @@ -1,654 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, - killAllSTCoresOnly, -} = require("./utils"); -let assert = require("assert"); -let { Querier } = require("../lib/build/querier"); -const nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); -let SuperTokens = require("../"); -let Session = require("../recipe/session"); -let SessionFunctions = require("../lib/build/recipe/session/sessionFunctions"); -let { parseJWTWithoutSignatureVerification } = require("../lib/build/recipe/session/jwt"); -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -const { maxVersion } = require("../lib/build/utils"); -const { fail } = require("assert"); - -/* TODO: -- the opposite of the above (check that if signing key changes, things are still fine) condition -- calling createNewSession twice, should overwrite the first call (in terms of cookies) -- calling createNewSession in the case of unauthorised error, should create a proper session -- revoking old session after create new session, should not remove new session's cookies. -- check that Access-Control-Expose-Headers header is being set properly during create, use and destroy session**** only for express -*/ - -describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( - "[test/sessionAccessTokenSigningKeyUpdate.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("check that if signing key changes, things are still fine", async function () { - await setKeyValueInConfig("access_token_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, "2.8") !== "2.8"; - - let response = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - response.antiCsrfToken, - true, - true - ); - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState === undefined); - } - - await new Promise((r) => setTimeout(r, 6000)); - - try { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - response.antiCsrfToken, - true, - true - ); - // Old core versions should throw here because the signing key was updated - if (!coreSupportsMultipleSignigKeys) { - fail(); - } - } catch (err) { - if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { - throw err; - } else if (coreSupportsMultipleSignigKeys) { - // Cores supporting multiple signig shouldn't throw since the signing key is still valid - fail(); - } - } - - const verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState === undefined); - - ProcessState.getInstance().reset(); - - const response2 = await SessionFunctions.refreshSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - response.refreshToken.token, - response.antiCsrfToken, - true, - "cookie", - "cookie" - ); - - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response2.accessToken.token), - response2.antiCsrfToken, - true, - true - ); - - // We call verify, since refresh does not refresh the signing key info - const verifyState2 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState2 !== undefined); - }); - - it("check that if signing key changes, after new key is fetched - via token query, old tokens don't query the core", async function () { - await setKeyValueInConfig("access_token_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, "2.8") !== "2.8"; - - const oldSession = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - await new Promise((r) => setTimeout(r, 6000)); - let originalHandShakeInfo = ( - await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() - ).clone(); - - const newSession = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.setHandshakeInfo(originalHandShakeInfo); - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(newSession.accessToken.token), - newSession.antiCsrfToken, - true, - true - ); - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - - if (!coreSupportsMultipleSignigKeys) { - assert(verifyState === undefined); - } else { - // We call verify here, since this is a new session we can't verify locally - assert(verifyState !== undefined); - } - } - - await ProcessState.getInstance().reset(); - - { - try { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(oldSession.accessToken.token), - oldSession.antiCsrfToken, - true, - true - ); - // Old core versions should throw here because the signing key was updated - if (!coreSupportsMultipleSignigKeys) { - fail(); - } - } catch (err) { - if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { - throw err; - } else if (coreSupportsMultipleSignigKeys) { - // Cores supporting multiple signig shouldn't throw since the signing key is still valid - fail(); - } - } - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState === undefined); - } - }); - - it("check that if signing key changes, after new key is fetched - via creation of new token, old tokens don't query the core", async function () { - await setKeyValueInConfig("access_token_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, "2.8") !== "2.8"; - - let response2 = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - await new Promise((r) => setTimeout(r, 6000)); - - let response = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - response.antiCsrfToken, - true, - true - ); - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState === undefined); - } - - await ProcessState.getInstance().reset(); - - { - try { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response2.accessToken.token), - response2.antiCsrfToken, - true, - true - ); - // Old core versions should throw here because the signing key was updated - if (!coreSupportsMultipleSignigKeys) { - fail(); - } - } catch (err) { - if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { - throw err; - } else if (coreSupportsMultipleSignigKeys) { - // Cores supporting multiple signig shouldn't throw since the signing key is still valid - fail(); - } - } - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState === undefined); - } - }); - - it("check that if signing key changes, after new key is fetched - via verification of old token, old tokens don't query the core", async function () { - await setKeyValueInConfig("access_token_signing_key_update_interval", "0.001"); // 5 seconds is the update interval - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, "2.8") !== "2.8"; - - let response2 = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - await new Promise((r) => setTimeout(r, 6000)); - - let originalHandShakeInfo = ( - await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() - ).clone(); - - let response = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - // we reset the handshake info to before the session creation so it's - // like the above session was created from another server. - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.setHandshakeInfo(originalHandShakeInfo); - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response.accessToken.token), - response.antiCsrfToken, - true, - true - ); - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - if (!coreSupportsMultipleSignigKeys) { - assert(verifyState === undefined); - } else { - assert(verifyState !== undefined); - } - } - - await ProcessState.getInstance().reset(); - - { - try { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(response2.accessToken.token), - response2.antiCsrfToken, - true, - true - ); - - // Old core versions should throw here because the signing key was updated - if (!coreSupportsMultipleSignigKeys) { - fail(); - } - } catch (err) { - if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { - throw err; - } else if (coreSupportsMultipleSignigKeys) { - // Cores supporting multiple signig shouldn't throw since the signing key is still valid - fail(); - } - } - - let verifyState = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState === undefined); - } - }); - - it("test reducing access token signing key update interval time", async function () { - await setKeyValueInConfig("access_token_signing_key_update_interval", "0.0041"); // 10 seconds - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let session = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(session.accessToken.token), - session.antiCsrfToken, - true, - true - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState3 === undefined); - } - - // we kill the core - await killAllSTCoresOnly(); - await setupST(); - - // start server again - await startST(); - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(session.accessToken.token), - session.antiCsrfToken, - true, - true - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState3 === undefined); - } - - // now we create a new session that will use a new key and we will - // do it in a way that the jwtSigningKey info is not updated (as if another server has created this new session) - let originalHandShakeInfo = ( - await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() - ).clone(); - - let session2 = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - // we reset the handshake info to before the session creation so it's - // like the above session was created from another server. - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.setHandshakeInfo(originalHandShakeInfo); - - // now we will call getSession on session2 and see that the core is called - { - // jwt signing key has not expired, according to the SDK, so it should succeed - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(session2.accessToken.token), - session2.antiCsrfToken, - true, - true - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState3 !== undefined); - } - - ProcessState.getInstance().reset(); - - // we will do the same thing, but this time core should not be called - { - // jwt signing key has not expired, according to the SDK, so it should succeed - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(session2.accessToken.token), - session2.antiCsrfToken, - true, - true - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState3 === undefined); - } - - { - // now we will use the original session again and see that core is not called - try { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(session.accessToken.token), - session.antiCsrfToken, - true, - true - ); - fail(); - } catch (err) { - if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { - throw err; - } - } - - let verifyState3 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState3 === undefined); - } - }); - - it("no access token signing key update", async function () { - await setKeyValueInConfig("access_token_signing_key_update_interval", "0.0011"); // 4 seconds - await setKeyValueInConfig("access_token_signing_key_dynamic", "false"); - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - let q = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await q.getAPIVersion(); - - // Only run test for >= 2.8 since the fix for this test is in core with CDI >= 2.8 - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - let session = await SessionFunctions.createNewSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - "", - false, - {}, - {} - ); - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(session.accessToken.token), - session.antiCsrfToken, - true, - true - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState3 === undefined); - } - - await new Promise((r) => setTimeout(r, 5000)); // wait for 5 seconds - - // it should not query the core anymore even if the jwtSigningKetUpdate interval has passed - - { - await SessionFunctions.getSession( - SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, - parseJWTWithoutSignatureVerification(session.accessToken.token), - session.antiCsrfToken, - true, - true - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent( - PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, - 1500 - ); - assert(verifyState3 === undefined); - } - }); -}); diff --git a/test/sessionAccessTokenSigningKeyUpdate.test.ts b/test/sessionAccessTokenSigningKeyUpdate.test.ts new file mode 100644 index 000000000..bd955b82c --- /dev/null +++ b/test/sessionAccessTokenSigningKeyUpdate.test.ts @@ -0,0 +1,655 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert, { fail } from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { Querier } from 'supertokens-node/querier' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import * as SessionFunctions from 'supertokens-node/recipe/session/sessionFunctions' +import { parseJWTWithoutSignatureVerification } from 'supertokens-node/recipe/session/jwt' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { maxVersion } from 'supertokens-node/utils' +import { + cleanST, + killAllST, + killAllSTCoresOnly, + printPath, + setKeyValueInConfig, + setupST, + startST, +} from './utils' + +/* TODO: +- the opposite of the above (check that if signing key changes, things are still fine) condition +- calling createNewSession twice, should overwrite the first call (in terms of cookies) +- calling createNewSession in the case of unauthorised error, should create a proper session +- revoking old session after create new session, should not remove new session's cookies. +- check that Access-Control-Expose-Headers header is being set properly during create, use and destroy session**** only for express +*/ + +describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( + '[test/sessionAccessTokenSigningKeyUpdate.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('check that if signing key changes, things are still fine', async () => { + await setKeyValueInConfig('access_token_signing_key_update_interval', '0.001') // 5 seconds is the update interval + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, '2.8') !== '2.8' + + const response = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + response.antiCsrfToken, + true, + true, + ) + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState === undefined) + } + + await new Promise(r => setTimeout(r, 6000)) + + try { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + response.antiCsrfToken, + true, + true, + ) + // Old core versions should throw here because the signing key was updated + if (!coreSupportsMultipleSignigKeys) + fail() + } + catch (err) { + if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { + throw err + } + else if (coreSupportsMultipleSignigKeys) { + // Cores supporting multiple signig shouldn't throw since the signing key is still valid + fail() + } + } + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState === undefined) + + ProcessState.getInstance().reset() + + const response2 = await SessionFunctions.refreshSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + response.refreshToken.token, + response.antiCsrfToken, + true, + 'cookie', + 'cookie', + ) + + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response2.accessToken.token), + response2.antiCsrfToken, + true, + true, + ) + + // We call verify, since refresh does not refresh the signing key info + const verifyState2 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState2 !== undefined) + }) + + it('check that if signing key changes, after new key is fetched - via token query, old tokens don\'t query the core', async () => { + await setKeyValueInConfig('access_token_signing_key_update_interval', '0.001') // 5 seconds is the update interval + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, '2.8') !== '2.8' + + const oldSession = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + await new Promise(r => setTimeout(r, 6000)) + const originalHandShakeInfo = ( + await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() + ).clone() + + const newSession = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.setHandshakeInfo(originalHandShakeInfo) + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(newSession.accessToken.token), + newSession.antiCsrfToken, + true, + true, + ) + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + + if (!coreSupportsMultipleSignigKeys) { + assert(verifyState === undefined) + } + else { + // We call verify here, since this is a new session we can't verify locally + assert(verifyState !== undefined) + } + } + + await ProcessState.getInstance().reset() + + { + try { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(oldSession.accessToken.token), + oldSession.antiCsrfToken, + true, + true, + ) + // Old core versions should throw here because the signing key was updated + if (!coreSupportsMultipleSignigKeys) + fail() + } + catch (err) { + if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { + throw err + } + else if (coreSupportsMultipleSignigKeys) { + // Cores supporting multiple signig shouldn't throw since the signing key is still valid + fail() + } + } + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState === undefined) + } + }) + + it('check that if signing key changes, after new key is fetched - via creation of new token, old tokens don\'t query the core', async () => { + await setKeyValueInConfig('access_token_signing_key_update_interval', '0.001') // 5 seconds is the update interval + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, '2.8') !== '2.8' + + const response2 = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + await new Promise(r => setTimeout(r, 6000)) + + const response = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + response.antiCsrfToken, + true, + true, + ) + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState === undefined) + } + + await ProcessState.getInstance().reset() + + { + try { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response2.accessToken.token), + response2.antiCsrfToken, + true, + true, + ) + // Old core versions should throw here because the signing key was updated + if (!coreSupportsMultipleSignigKeys) + fail() + } + catch (err) { + if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { + throw err + } + else if (coreSupportsMultipleSignigKeys) { + // Cores supporting multiple signig shouldn't throw since the signing key is still valid + fail() + } + } + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState === undefined) + } + }) + + it('check that if signing key changes, after new key is fetched - via verification of old token, old tokens don\'t query the core', async () => { + await setKeyValueInConfig('access_token_signing_key_update_interval', '0.001') // 5 seconds is the update interval + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + const coreSupportsMultipleSignigKeys = maxVersion(currCDIVersion, '2.8') !== '2.8' + + const response2 = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + await new Promise(r => setTimeout(r, 6000)) + + const originalHandShakeInfo = ( + await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() + ).clone() + + const response = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + // we reset the handshake info to before the session creation so it's + // like the above session was created from another server. + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.setHandshakeInfo(originalHandShakeInfo) + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response.accessToken.token), + response.antiCsrfToken, + true, + true, + ) + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + if (!coreSupportsMultipleSignigKeys) + assert(verifyState === undefined) + + else + assert(verifyState !== undefined) + } + + await ProcessState.getInstance().reset() + + { + try { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(response2.accessToken.token), + response2.antiCsrfToken, + true, + true, + ) + + // Old core versions should throw here because the signing key was updated + if (!coreSupportsMultipleSignigKeys) + fail() + } + catch (err) { + if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { + throw err + } + else if (coreSupportsMultipleSignigKeys) { + // Cores supporting multiple signig shouldn't throw since the signing key is still valid + fail() + } + } + + const verifyState = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState === undefined) + } + }) + + it('test reducing access token signing key update interval time', async () => { + await setKeyValueInConfig('access_token_signing_key_update_interval', '0.0041') // 10 seconds + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const session = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(session.accessToken.token), + session.antiCsrfToken, + true, + true, + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState3 === undefined) + } + + // we kill the core + await killAllSTCoresOnly() + await setupST() + + // start server again + await startST() + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(session.accessToken.token), + session.antiCsrfToken, + true, + true, + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState3 === undefined) + } + + // now we create a new session that will use a new key and we will + // do it in a way that the jwtSigningKey info is not updated (as if another server has created this new session) + const originalHandShakeInfo = ( + await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getHandshakeInfo() + ).clone() + + const session2 = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + // we reset the handshake info to before the session creation so it's + // like the above session was created from another server. + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.setHandshakeInfo(originalHandShakeInfo) + + // now we will call getSession on session2 and see that the core is called + { + // jwt signing key has not expired, according to the SDK, so it should succeed + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(session2.accessToken.token), + session2.antiCsrfToken, + true, + true, + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState3 !== undefined) + } + + ProcessState.getInstance().reset() + + // we will do the same thing, but this time core should not be called + { + // jwt signing key has not expired, according to the SDK, so it should succeed + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(session2.accessToken.token), + session2.antiCsrfToken, + true, + true, + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState3 === undefined) + } + + { + // now we will use the original session again and see that core is not called + try { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(session.accessToken.token), + session.antiCsrfToken, + true, + true, + ) + fail() + } + catch (err) { + if (err.type !== Session.Error.TRY_REFRESH_TOKEN) + throw err + } + + const verifyState3 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState3 === undefined) + } + }) + + it('no access token signing key update', async () => { + await setKeyValueInConfig('access_token_signing_key_update_interval', '0.0011') // 4 seconds + await setKeyValueInConfig('access_token_signing_key_dynamic', 'false') + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const q = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await q.getAPIVersion() + + // Only run test for >= 2.8 since the fix for this test is in core with CDI >= 2.8 + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + const session = await SessionFunctions.createNewSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + '', + false, + {}, + {}, + ) + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(session.accessToken.token), + session.antiCsrfToken, + true, + true, + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState3 === undefined) + } + + await new Promise(r => setTimeout(r, 5000)) // wait for 5 seconds + + // it should not query the core anymore even if the jwtSigningKetUpdate interval has passed + + { + await SessionFunctions.getSession( + SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, + parseJWTWithoutSignatureVerification(session.accessToken.token), + session.antiCsrfToken, + true, + true, + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent( + PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, + 1500, + ) + assert(verifyState3 === undefined) + } + }) +}) diff --git a/test/sessionExpress.test.js b/test/sessionExpress.test.js deleted file mode 100644 index 979b75131..000000000 --- a/test/sessionExpress.test.js +++ /dev/null @@ -1,3179 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, - delay, -} = require("./utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); -let SuperTokens = require("../"); -let Session = require("../recipe/session"); -let { Querier } = require("../lib/build/querier"); -const { default: NormalisedURLPath } = require("../lib/build/normalisedURLPath"); -const { verifySession } = require("../recipe/session/framework/express"); -const { default: next } = require("next"); -let { middleware, errorHandler } = require("../framework/express"); - -describe(`sessionExpress: ${printPath("[test/sessionExpress.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // check if disabling api, the default refresh API does not work - you get a 404 - it("test that if disabling api, the default refresh API does not work", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .set("st-auth-mode", "cookie") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.status === 404); - }); - - it("test that if disabling api, the default sign out API does not work", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .set("st-auth-mode", "cookie") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(res2.status === 404); - }); - - //- check for token theft detection - it("express token theft detection", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, request, response) => { - response.sendJSONResponse({ - success: true, - }); - }, - }, - antiCsrf: "VIA_TOKEN", - }), - ], - }); - - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - res.status(200).send(""); - }); - - app.post("/auth/session/refresh", async (req, res, next) => { - try { - await Session.refreshSession(req, res); - res.status(200).send(JSON.stringify({ success: false })); - } catch (err) { - next(err); - } - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - resolve(); - }) - ); - - let res3 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res3.body.success, true); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(cookies.accessTokenDomain === undefined); - assert(cookies.refreshTokenDomain === undefined); - }); - - //- check for token theft detection - it("express token theft detection with auto refresh middleware", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - resolve(); - }) - ); - - let res3 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(res3.status === 401); - assert.deepStrictEqual(res3.text, '{"message":"token theft detected"}'); - - let cookies = extractInfoFromResponse(res3); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, ""); - assert.strictEqual(cookies.refreshToken, ""); - assert.strictEqual(cookies.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(cookies.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - }); - - //check basic usage of session - it("test basic usage of express sessions", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - res.status(200).send(""); - }); - app.post("/auth/session/refresh", async (req, res) => { - await Session.refreshSession(req, res); - res.status(200).send(""); - }); - app.post("/session/revoke", async (req, res) => { - let session = await Session.getSession(req, res); - await session.revokeSession(); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test basic usage of express sessions with headers", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - getTokenTransferMethod: () => "header", - }), - ], - }); - - const app = express(); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", async (req, res) => { - await Session.getSession(req, res); - res.status(200).send(""); - }); - app.post("/auth/session/refresh", async (req, res) => { - await Session.refreshSession(req, res); - res.status(200).send(""); - }); - app.post("/session/revoke", async (req, res) => { - let session = await Session.getSession(req, res); - await session.revokeSession(); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.antiCsrf === undefined); - - assert.ok(res.accessTokenFromHeader); - assert.strictEqual(res.accessToken, undefined); - - assert.ok(res.refreshTokenFromHeader); - assert.strictEqual(res.refreshToken, undefined); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Authorization", `Bearer ${res.accessTokenFromHeader}`) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Authorization", `Bearer ${res.refreshTokenFromHeader}`) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res2.antiCsrf === undefined); - assert.ok(res2.accessTokenFromHeader); - assert.strictEqual(res2.accessToken, undefined); - - assert.ok(res2.refreshTokenFromHeader); - assert.strictEqual(res2.refreshToken, undefined); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Authorization", `Bearer ${res2.accessTokenFromHeader}`) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessTokenFromHeader !== undefined); - - ProcessState.getInstance().reset(); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Authorization", `Bearer ${res3.accessTokenFromHeader}`) - - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/session/revoke") - .set("Authorization", `Bearer ${res3.accessTokenFromHeader}`) - - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - - assert.strictEqual(sessionRevokedResponseExtracted.accessTokenFromHeader, ""); - assert.strictEqual(sessionRevokedResponseExtracted.refreshTokenFromHeader, ""); - }); - - it("test that if accessTokenPath is set to custom /access, then path of accessToken from session is equal to this", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - accessTokenPath: "/access", - }), - ], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - const res = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; - cookies = cookies === undefined ? [] : cookies; - - const paths = cookies.filter((item) => item.startsWith("sAccessToken"))[0].split("; "); - assert.strictEqual( - ["path", "Path"].flatMap((need) => paths.filter((actual) => actual.startsWith(need)))[0].split("=")[1], - "/access" - ); - }); - - it("test that if default accessTokenPath is used, then path of accessToken from session is equal to slash", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - }), - ], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - const res = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; - cookies = cookies === undefined ? [] : cookies; - - const paths = cookies.filter((item) => item.startsWith("sAccessToken"))[0].split("; "); - assert.strictEqual( - ["path", "Path"].flatMap((need) => paths.filter((actual) => actual.startsWith(need)))[0].split("=")[1], - "/" - ); - }); - - it("test signout API works", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test signout API works if even session is deleted on the backend after creation", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - const app = express(); - app.use(middleware()); - - let sessionHandle = ""; - - app.post("/create", async (req, res) => { - let session = await Session.createNewSession(req, res, "", {}, {}); - sessionHandle = session.getHandle(); - res.status(200).send(""); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await Session.revokeSession(sessionHandle); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - //check basic usage of session - it("test basic usage of express sessions with auto refresh", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.post("/session/revoke", verifySession(), async (req, res) => { - let session = req.session; - await session.revokeSession(); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res2.accessToken !== undefined); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - - ProcessState.getInstance().reset(); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - //check session verify for with / without anti-csrf present - it("test express session verify with anti-csrf present", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", async (req, res) => { - let sessionResponse = await Session.getSession(req, res); - res.status(200).json({ userId: sessionResponse.userId }); - }); - - app.post("/session/verifyAntiCsrfFalse", async (req, res) => { - let sessionResponse = await Session.getSession(req, res, false); - res.status(200).json({ userId: sessionResponse.userId }); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res2.body.userId, "id1"); - - let res3 = await new Promise((resolve) => - request(app) - .post("/session/verifyAntiCsrfFalse") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res3.body.userId, "id1"); - }); - - // check session verify for with / without anti-csrf present - it("test session verify without anti-csrf present express", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", async (req, res) => { - try { - let sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: true }); - res.status(200).json({ success: false }); - } catch (err) { - res.status(200).json({ - success: err.type === Session.Error.TRY_REFRESH_TOKEN, - }); - } - }); - - app.post("/session/verifyAntiCsrfFalse", async (req, res) => { - let sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: false }); - res.status(200).json({ userId: sessionResponse.userId }); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let response2 = await new Promise((resolve) => - request(app) - .post("/session/verifyAntiCsrfFalse") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response2.body.userId, "id1"); - - let response = await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.body.success, true); - }); - - //check revoking session(s)** - it("test revoking express sessions", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - app.post("/usercreate", async (req, res) => { - await Session.createNewSession(req, res, "someUniqueUserId", {}, {}); - res.status(200).send(""); - }); - app.post("/session/revoke", async (req, res) => { - let session = await Session.getSession(req, res); - await session.revokeSession(); - res.status(200).send(""); - }); - - app.post("/session/revokeUserid", async (req, res) => { - let session = await Session.getSession(req, res); - await Session.revokeAllSessionsForUser(session.getUserId()); - res.status("200").send(""); - }); - - //create an api call get sesssions from a userid "id1" that returns all the sessions for that userid - app.post("/session/getSessionsWithUserId1", async (req, res) => { - let sessionHandles = await Session.getAllSessionHandlesForUser("someUniqueUserId"); - res.status(200).json(sessionHandles); - }); - - let response = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - - await new Promise((resolve) => - request(app) - .post("/usercreate") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let userCreateResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/usercreate") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - await new Promise((resolve) => - request(app) - .post("/session/revokeUserid") - .set("Cookie", ["sAccessToken=" + userCreateResponse.accessToken]) - .set("anti-csrf", userCreateResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionHandleResponse = await new Promise((resolve) => - request(app) - .post("/session/getSessionsWithUserId1") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(sessionHandleResponse.body.length === 0); - }); - - //check manipulating session data - it("test manipulating session data with express", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - app.post("/updateSessionData", async (req, res) => { - let session = await Session.getSession(req, res); - await session.updateSessionData({ key: "value" }); - res.status(200).send(""); - }); - app.post("/getSessionData", async (req, res) => { - let session = await Session.getSession(req, res); - let sessionData = await session.getSessionData(); - res.status(200).json(sessionData); - }); - - app.post("/updateSessionData2", async (req, res) => { - let session = await Session.getSession(req, res); - await session.updateSessionData(null); - res.status(200).send(""); - }); - - app.post("/updateSessionDataInvalidSessionHandle", async (req, res) => { - res.status(200).json({ success: !(await Session.updateSessionData("InvalidHandle", { key: "value3" })) }); - }); - - //create a new session - let response = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - //call the updateSessionData api to add session data - await new Promise((resolve) => - request(app) - .post("/updateSessionData") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //call the getSessionData api to get session data - let response2 = await new Promise((resolve) => - request(app) - .post("/getSessionData") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //check that the session data returned is valid - assert.strictEqual(response2.body.key, "value"); - - // change the value of the inserted session data - await new Promise((resolve) => - request(app) - .post("/updateSessionData2") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - //retrieve the changed session data - response2 = await new Promise((resolve) => - request(app) - .post("/getSessionData") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //check the value of the retrieved - assert.deepStrictEqual(response2.body, {}); - - //invalid session handle when updating the session data - let invalidSessionResponse = await new Promise((resolve) => - request(app) - .post("/updateSessionDataInvalidSessionHandle") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(invalidSessionResponse.body.success, true); - }); - - //check manipulating jwt payload - it("test manipulating jwt payload with express", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "user1", {}, {}); - res.status(200).send(""); - }); - app.post("/updateAccessTokenPayload", async (req, res) => { - let session = await Session.getSession(req, res); - let accessTokenBefore = session.accessToken; - await session.updateAccessTokenPayload({ key: "value" }); - let accessTokenAfter = session.accessToken; - let statusCode = accessTokenBefore !== accessTokenAfter && typeof accessTokenAfter === "string" ? 200 : 500; - res.status(statusCode).send(""); - }); - app.post("/auth/session/refresh", async (req, res) => { - await Session.refreshSession(req, res); - res.status(200).send(""); - }); - app.post("/getAccessTokenPayload", async (req, res) => { - let session = await Session.getSession(req, res); - let jwtPayload = session.getAccessTokenPayload(); - res.status(200).json(jwtPayload); - }); - - app.post("/updateAccessTokenPayload2", async (req, res) => { - let session = await Session.getSession(req, res); - await session.updateAccessTokenPayload(null); - res.status(200).send(""); - }); - - app.post("/updateAccessTokenPayloadInvalidSessionHandle", async (req, res) => { - res.status(200).json({ - success: !(await Session.updateAccessTokenPayload("InvalidHandle", { key: "value3" })), - }); - }); - - //create a new session - let response = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //call the updateAccessTokenPayload api to add jwt payload - let updatedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/updateAccessTokenPayload") - .set("Cookie", ["sAccessToken=" + response.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - //call the getAccessTokenPayload api to get jwt payload - let response2 = await new Promise((resolve) => - request(app) - .post("/getAccessTokenPayload") - .set("Cookie", ["sAccessToken=" + updatedResponse.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - //check that the jwt payload returned is valid - assert.strictEqual(response2.body.key, "value"); - - // refresh session - response2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + response.refreshToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, { key: "value" }); - - // change the value of the inserted jwt payload - let updatedResponse2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/updateAccessTokenPayload2") - .set("Cookie", ["sAccessToken=" + response2.accessToken]) - .set("anti-csrf", response2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, "base64").toString()); - assert(frontendInfo.uid === "user1"); - assert.deepStrictEqual(frontendInfo.up, {}); - - //retrieve the changed jwt payload - response2 = await new Promise((resolve) => - request(app) - .post("/getAccessTokenPayload") - .set("Cookie", ["sAccessToken=" + updatedResponse2.accessToken]) - .set("anti-csrf", response2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - //check the value of the retrieved - assert.deepStrictEqual(response2.body, {}); - //invalid session handle when updating the jwt payload - let invalidSessionResponse = await new Promise((resolve) => - request(app) - .post("/updateAccessTokenPayloadInvalidSessionHandle") - .set("Cookie", ["sAccessToken=" + updatedResponse2.accessToken]) - .set("anti-csrf", response.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(invalidSessionResponse.body.success, true); - }); - - // test with existing header params being there and that the lib appends to those and not overrides those - it("test that express appends to existing header params and does not override", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - const app = express(); - app.post("/create", async (req, res) => { - res.header("testHeader", "testValue"); - res.header("Access-Control-Expose-Headers", "customValue"); - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - //create a new session - - let response = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.headers.testheader, "testValue"); - assert.deepEqual(response.headers["access-control-expose-headers"], "customValue, front-token, anti-csrf"); - - //normal session headers - let extractInfo = extractInfoFromResponse(response); - assert(extractInfo.accessToken !== undefined); - assert(extractInfo.refreshToken != undefined); - assert(extractInfo.antiCsrf !== undefined); - }); - - //if anti-csrf is disabled from ST core, check that not having that in input to verify session is fine** - it("test that when anti-csrf is disabled from from ST core, not having to input in verify session is fine in express", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "NONE" })], - }); - - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", async (req, res) => { - let sessionResponse = await Session.getSession(req, res); - res.status(200).json({ userId: sessionResponse.userId }); - }); - app.post("/session/verifyAntiCsrfFalse", async (req, res) => { - let sessionResponse = await Session.getSession(req, res, false); - res.status(200).json({ userId: sessionResponse.userId }); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - let res2 = await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res2.body.userId, "id1"); - - let res3 = await new Promise((resolve) => - request(app) - .post("/session/verifyAntiCsrfFalse") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(res3.body.userId, "id1"); - }); - - it("test that getSession does not clear cookies if a session does not exist in the first place", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.post("/session/verify", async (req, res) => { - try { - await Session.getSession(req, res); - } catch (err) { - if (err.type === Session.Error.UNAUTHORISED) { - res.status(200).json({ success: true }); - return; - } - } - res.status(200).json({ success: false }); - }); - - let res = await new Promise((resolve) => - request(app) - .post("/session/verify") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.strictEqual(res.body.success, true); - - let cookies = extractInfoFromResponse(res); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, undefined); - assert.strictEqual(cookies.refreshToken, undefined); - assert.strictEqual(cookies.accessTokenExpiry, undefined); - assert.strictEqual(cookies.refreshTokenExpiry, undefined); - }); - - it("test that refreshSession does not clear cookies if a session does not exist in the first place", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - - const app = express(); - - app.post("/auth/session/refresh", async (req, res) => { - try { - await Session.refreshSession(req, res); - } catch (err) { - if (err.type === Session.Error.UNAUTHORISED) { - res.status(200).json({ success: true }); - return; - } - } - res.status(200).json({ success: false }); - }); - - let res = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.strictEqual(res.body.success, true); - - let cookies = extractInfoFromResponse(res); - assert.strictEqual(cookies.antiCsrf, undefined); - assert.strictEqual(cookies.accessToken, undefined); - assert.strictEqual(cookies.refreshToken, undefined); - assert.strictEqual(cookies.accessTokenExpiry, undefined); - assert.strictEqual(cookies.refreshTokenExpiry, undefined); - }); - - it("test that when anti-csrf is enabled with custom header, and we don't provide that in verifySession, we get try refresh token", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_CUSTOM_HEADER" })], - }); - - const app = express(); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - let sessionResponse = req.session; - res.status(200).json({ userId: sessionResponse.userId }); - }); - app.post("/session/verifyAntiCsrfFalse", verifySession({ antiCsrfCheck: false }), async (req, res) => { - let sessionResponse = req.session; - res.status(200).json({ userId: sessionResponse.userId }); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - { - let res2 = await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.deepStrictEqual(res2.status, 401); - assert.deepStrictEqual(res2.text, '{"message":"try refresh token"}'); - - let res3 = await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("rid", "session") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.deepStrictEqual(res3.body.userId, "id1"); - } - - { - let res2 = await new Promise((resolve) => - request(app) - .post("/session/verifyAntiCsrfFalse") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.deepStrictEqual(res2.body.userId, "id1"); - - let res3 = await new Promise((resolve) => - request(app) - .post("/session/verifyAntiCsrfFalse") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("rid", "session") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.deepStrictEqual(res3.body.userId, "id1"); - } - }); - - it("test resfresh API when using CUSTOM HEADER anti-csrf", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_CUSTOM_HEADER" })], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - { - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.deepStrictEqual(res2.status, 401); - assert.deepStrictEqual(res2.text, '{"message":"unauthorised"}'); - } - - { - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("rid", "session") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.deepStrictEqual(res2.status, 200); - } - }); - - it("test that init can be called post route and middleware declaration", async function () { - await startST(); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "id1", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - let sessionResponse = req.session; - res.status(200).json({ userId: sessionResponse.userId }); - }); - app.post("/session/verifyAntiCsrfFalse", verifySession(false), async (req, res) => { - let sessionResponse = req.session; - res.status(200).json({ userId: sessionResponse.userId }); - }); - - app.use(errorHandler()); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_CUSTOM_HEADER" })], - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - { - let res2 = await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.deepStrictEqual(res2.status, 401); - assert.deepStrictEqual(res2.text, '{"message":"try refresh token"}'); - - let res3 = await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("rid", "session") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.deepStrictEqual(res3.body.userId, "id1"); - } - }); - - it("test overriding of sessions functions", async function () { - await startST(); - - let createNewSessionCalled = false; - let getSessionCalled = false; - let refreshSessionCalled = false; - let session = undefined; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => { - return { - ...oI, - createNewSession: async (input) => { - let response = await oI.createNewSession(input); - createNewSessionCalled = true; - session = response; - return response; - }, - getSession: async (input) => { - let response = await oI.getSession(input); - getSessionCalled = true; - session = response; - return response; - }, - refreshSession: async (input) => { - let response = await oI.refreshSession(input); - refreshSessionCalled = true; - session = response; - return response; - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.post("/session/revoke", verifySession(), async (req, res) => { - let session = req.session; - await session.revokeSession(); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(createNewSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert(res.accessToken !== undefined); - assert.strictEqual(session.getAccessToken(), decodeURIComponent(res.accessToken)); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - session = undefined; - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); - assert(verifyState3 === undefined); - - let res2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(refreshSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert(res2.accessToken !== undefined); - assert.strictEqual(session.getAccessToken(), decodeURIComponent(res2.accessToken)); - assert(res2.antiCsrf !== undefined); - assert(res2.refreshToken !== undefined); - session = undefined; - - let res3 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); - assert.strictEqual(getSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert(verifyState !== undefined); - assert(res3.accessToken !== undefined); - assert.strictEqual(session.getAccessToken(), decodeURIComponent(res3.accessToken)); - - ProcessState.getInstance().reset(); - - await new Promise((resolve) => - request(app) - .post("/session/verify") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); - assert(verifyState2 === undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/session/revoke") - .set("Cookie", ["sAccessToken=" + res3.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test overriding of sessions apis", async function () { - await startST(); - - let signoutCalled = false; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: async (input) => { - let response = await oI.signOutPOST(input); - signoutCalled = true; - return response; - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - let sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse); - assert.strictEqual(signoutCalled, true); - assert(sessionRevokedResponseExtracted.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(sessionRevokedResponseExtracted.accessToken === ""); - assert(sessionRevokedResponseExtracted.refreshToken === ""); - }); - - it("test overriding of sessions functions, error thrown", async function () { - await startST(); - - let createNewSessionCalled = false; - let session = undefined; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - functions: (oI) => { - return { - ...oI, - createNewSession: async (input) => { - let response = await oI.createNewSession(input); - createNewSessionCalled = true; - session = response; - throw { - error: "create new session error", - }; - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res, next) => { - try { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - } catch (err) { - next(err); - } - }); - - app.use(errorHandler()); - - app.use((err, req, res, next) => { - res.json({ - customError: true, - ...err, - }); - }); - - let res = await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.strictEqual(createNewSessionCalled, true); - assert.notStrictEqual(session, undefined); - assert.deepStrictEqual(res, { customError: true, error: "create new session error" }); - }); - - it("test overriding of sessions apis, error thrown", async function () { - await startST(); - - let signoutCalled = false; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: async (input) => { - let response = await oI.signOutPOST(input); - signoutCalled = true; - throw { - error: "signout error", - }; - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.post("/session/verify", verifySession(), async (req, res) => { - res.status(200).send(""); - }); - - app.use(errorHandler()); - - app.use((err, req, res, next) => { - res.json({ - customError: true, - ...err, - }); - }); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let sessionRevokedResponse = await new Promise((resolve) => - request(app) - .post("/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.strictEqual(signoutCalled, true); - assert.deepStrictEqual(sessionRevokedResponse, { customError: true, error: "signout error" }); - }); - - it("check that refresh doesn't clear cookies if missing anti csrf via custom header", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_CUSTOM_HEADER" })], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - { - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.deepStrictEqual(res2.status, 401); - assert.deepStrictEqual(res2.text, '{"message":"unauthorised"}'); - let sessionRevokedResponseExtracted = extractInfoFromResponse(res2); - } - }); - - it("check that refresh doesn't clear cookies if missing anti csrf via token", async function () { - await startST(); - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], - }); - const app = express(); - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - { - let res2 = await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.deepStrictEqual(res2.status, 401); - assert.deepStrictEqual(res2.text, '{"message":"unauthorised"}'); - } - }); - - it("test session error handler overriding", async function () { - await startST(); - let testpass = false; - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ - getTokenTransferMethod: () => "cookie", - antiCsrf: "VIA_TOKEN", - errorHandlers: { - onUnauthorised: async (message, request, response) => { - await new Promise((r) => - setTimeout(() => { - testpass = true; - r(); - }, 5000) - ); - throw Error("onUnauthorised error caught"); - }, - }, - }), - ], - }); - - const app = express(); - - app.post("/session/verify", async (req, res, next) => { - try { - await Session.getSession(req, res); - res.status(200).send(""); - } catch (err) { - next(err); - } - }); - - app.use(errorHandler()); - - app.use((err, req, res, next) => { - if (err.message === "onUnauthorised error caught") { - res.status(403); - res.json({}); - } - }); - - let response = await new Promise((resolve) => - request(app) - .post("/session/verify") - .expect(403) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.status, 403); - assert(testpass); - }); - - it("test revoking a session during refresh with revokeSession function", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: async function (input) { - let session = await oI.refreshPOST(input); - await session.revokeSession(); - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .set("st-auth-mode", "cookie") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.notStrictEqual(res.accessToken, undefined); - assert.notStrictEqual(res.antiCsrf, undefined); - assert.notStrictEqual(res.refreshToken, undefined); - - let resp = await new Promise((resolve) => - request(app) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(resp.status === 200); - - let res2 = extractInfoFromResponse(resp); - - assert.deepEqual(res2.accessToken, ""); - assert.deepEqual(res2.refreshToken, ""); - assert.deepEqual(res2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.deepEqual(res2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(res2.accessTokenDomain === undefined); - assert(res2.refreshTokenDomain === undefined); - assert.strictEqual(res2.frontToken, "remove"); - assert.strictEqual(res2.antiCsrf, undefined); - }); - - it("test revoking a session during refresh with revokeSession function and sending 401", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: async function (input) { - let session = await oI.refreshPOST(input); - await session.revokeSession(); - input.options.res.setStatusCode(401); - input.options.res.sendJSONResponse({}); - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .set("st-auth-mode", "cookie") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let resp = await new Promise((resolve) => - request(app) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(resp.status === 401); - - let res2 = extractInfoFromResponse(resp); - - assert.deepEqual(res2.accessToken, ""); - assert.deepEqual(res2.refreshToken, ""); - assert.deepEqual(res2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.deepEqual(res2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(res2.accessTokenDomain === undefined); - assert(res2.refreshTokenDomain === undefined); - assert.strictEqual(res2.frontToken, "remove"); - assert.strictEqual(res2.antiCsrf, undefined); - }); - - it("test revoking a session during refresh with throwing unauthorised error", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: async function (input) { - await oI.refreshPOST(input); - throw new Session.Error({ - message: "unauthorised", - type: Session.Error.UNAUTHORISED, - clearTokens: true, - }); - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .set("st-auth-mode", "cookie") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let resp = await new Promise((resolve) => - request(app) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken, "sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(resp.status === 401); - - let res2 = extractInfoFromResponse(resp); - - assert.strictEqual(res2.accessToken, ""); - assert.strictEqual(res2.refreshToken, ""); - assert.strictEqual(res2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(res2.accessTokenDomain, undefined); - assert.strictEqual(res2.refreshTokenDomain, undefined); - assert.strictEqual(res2.frontToken, "remove"); - assert.strictEqual(res2.antiCsrf, undefined); - }); - - it("test revoking a session during refresh fails if just sending 401", async function () { - await startST(); - - SuperTokens.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - apiBasePath: "/", - }, - recipeList: [ - Session.init({ - antiCsrf: "VIA_TOKEN", - override: { - apis: (oI) => { - return { - ...oI, - refreshPOST: async function (input) { - let session = await oI.refreshPOST(input); - input.options.res.setStatusCode(401); - input.options.res.sendJSONResponse({}); - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, "", {}, {}); - res.status(200).send(""); - }); - - app.use(errorHandler()); - - let res = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/create") - .set("st-auth-mode", "cookie") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(res.accessToken !== undefined); - assert(res.antiCsrf !== undefined); - assert(res.refreshToken !== undefined); - - let resp = await new Promise((resolve) => - request(app) - .post("/session/refresh") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(resp.status === 401); - - let res2 = extractInfoFromResponse(resp); - - assert(res2.accessToken.length > 1); - assert(res2.antiCsrf.length > 1); - assert(res2.refreshToken.length > 1); - }); -}); diff --git a/test/sessionExpress.test.ts b/test/sessionExpress.test.ts new file mode 100644 index 000000000..887383767 --- /dev/null +++ b/test/sessionExpress.test.ts @@ -0,0 +1,3178 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import express from 'express' +import request from 'supertest' +import { PROCESS_STATE, ProcessState } from 'supertokens-node/processState' +import SuperTokens from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { verifySession } from 'supertokens-node/recipe/session/framework/express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { + cleanST, + extractInfoFromResponse, + killAllST, + printPath, + setupST, + startST, +} from './utils' + +describe(`sessionExpress: ${printPath('[test/sessionExpress.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // check if disabling api, the default refresh API does not work - you get a 404 + it('test that if disabling api, the default refresh API does not work', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + refreshPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .set('st-auth-mode', 'cookie') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.status === 404) + }) + + it('test that if disabling api, the default sign out API does not work', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .set('st-auth-mode', 'cookie') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/auth/signout') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(res2.status === 404) + }) + + // - check for token theft detection + it('express token theft detection', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + errorHandlers: { + onTokenTheftDetected: async (sessionHandle, userId, request, response) => { + response.sendJSONResponse({ + success: true, + }) + }, + }, + antiCsrf: 'VIA_TOKEN', + }), + ], + }) + + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + res.status(200).send('') + }) + + app.post('/auth/session/refresh', async (req, res, next) => { + try { + await Session.refreshSession(req, res) + res.status(200).send(JSON.stringify({ success: false })) + } + catch (err) { + next(err) + } + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + resolve() + }), + ) + + const res3 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert.strictEqual(res3.body.success, true) + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(cookies.accessTokenDomain === undefined) + assert(cookies.refreshTokenDomain === undefined) + }) + + // - check for token theft detection + it('express token theft detection with auto refresh middleware', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + resolve() + }), + ) + + const res3 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert(res3.status === 401) + assert.deepStrictEqual(res3.text, '{"message":"token theft detected"}') + + const cookies = extractInfoFromResponse(res3) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, '') + assert.strictEqual(cookies.refreshToken, '') + assert.strictEqual(cookies.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(cookies.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + }) + + // check basic usage of session + it('test basic usage of express sessions', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + res.status(200).send('') + }) + app.post('/auth/session/refresh', async (req, res) => { + await Session.refreshSession(req, res) + res.status(200).send('') + }) + app.post('/session/revoke', async (req, res) => { + const session = await Session.getSession(req, res) + await session.revokeSession() + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test basic usage of express sessions with headers', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + getTokenTransferMethod: () => 'header', + }), + ], + }) + + const app = express() + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', async (req, res) => { + await Session.getSession(req, res) + res.status(200).send('') + }) + app.post('/auth/session/refresh', async (req, res) => { + await Session.refreshSession(req, res) + res.status(200).send('') + }) + app.post('/session/revoke', async (req, res) => { + const session = await Session.getSession(req, res) + await session.revokeSession() + res.status(200).send('') + }) + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.antiCsrf === undefined) + + assert.ok(res.accessTokenFromHeader) + assert.strictEqual(res.accessToken, undefined) + + assert.ok(res.refreshTokenFromHeader) + assert.strictEqual(res.refreshToken, undefined) + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Authorization', `Bearer ${res.accessTokenFromHeader}`) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Authorization', `Bearer ${res.refreshTokenFromHeader}`) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res2.antiCsrf === undefined) + assert.ok(res2.accessTokenFromHeader) + assert.strictEqual(res2.accessToken, undefined) + + assert.ok(res2.refreshTokenFromHeader) + assert.strictEqual(res2.refreshToken, undefined) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Authorization', `Bearer ${res2.accessTokenFromHeader}`) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessTokenFromHeader !== undefined) + + ProcessState.getInstance().reset() + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Authorization', `Bearer ${res3.accessTokenFromHeader}`) + + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/session/revoke') + .set('Authorization', `Bearer ${res3.accessTokenFromHeader}`) + + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + + assert.strictEqual(sessionRevokedResponseExtracted.accessTokenFromHeader, '') + assert.strictEqual(sessionRevokedResponseExtracted.refreshTokenFromHeader, '') + }) + + it("test that if accessTokenPath is set to custom /access, then path of accessToken from session is equal to this", async function () { + await startST(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => "cookie", + antiCsrf: "VIA_TOKEN", + accessTokenPath: "/access", + }), + ], + }); + const app = express(); + app.use(middleware()); + + app.post("/create", async (req, res) => { + await Session.createNewSession(req, res, "", {}, {}); + res.status(200).send(""); + }); + const res = await new Promise((resolve) => + request(app) + .post("/create") + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; + cookies = cookies === undefined ? [] : cookies; + + const paths = cookies.filter((item) => item.startsWith("sAccessToken"))[0].split("; "); + assert.strictEqual( + ["path", "Path"].flatMap((need) => paths.filter((actual) => actual.startsWith(need)))[0].split("=")[1], + "/access" + ); + }); + + it("test that if default accessTokenPath is used, then path of accessToken from session is equal to slash", async function () { + await startST(); + SuperTokens.init({ + supertokens: { + connectionURI: "http://localhost:8080", + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => "cookie", + antiCsrf: "VIA_TOKEN", + }), + ], + }); + const app = express(); + app.use(middleware()); + + app.post("/create", async (req, res) => { + await Session.createNewSession(req, res, "", {}, {}); + res.status(200).send(""); + }); + const res = await new Promise((resolve) => + request(app) + .post("/create") + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; + cookies = cookies === undefined ? [] : cookies; + + const paths = cookies.filter((item) => item.startsWith("sAccessToken"))[0].split("; "); + assert.strictEqual( + ["path", "Path"].flatMap((need) => paths.filter((actual) => actual.startsWith(need)))[0].split("=")[1], + "/" + ); + }); + + it('test signout API works', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test signout API works if even session is deleted on the backend after creation', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = express() + app.use(middleware()) + + let sessionHandle = '' + + app.post('/create', async (req, res) => { + const session = await Session.createNewSession(req, res, '', {}, {}) + sessionHandle = session.getHandle() + res.status(200).send('') + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + await Session.revokeSession(sessionHandle) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check basic usage of session + it('test basic usage of express sessions with auto refresh', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.post('/session/revoke', verifySession(), async (req, res) => { + const session = req.session + await session.revokeSession() + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res2.accessToken !== undefined) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + + ProcessState.getInstance().reset() + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + // check session verify for with / without anti-csrf present + it('test express session verify with anti-csrf present', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', async (req, res) => { + const sessionResponse = await Session.getSession(req, res) + res.status(200).json({ userId: sessionResponse.userId }) + }) + + app.post('/session/verifyAntiCsrfFalse', async (req, res) => { + const sessionResponse = await Session.getSession(req, res, false) + res.status(200).json({ userId: sessionResponse.userId }) + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(res2.body.userId, 'id1') + + const res3 = await new Promise(resolve => + request(app) + .post('/session/verifyAntiCsrfFalse') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(res3.body.userId, 'id1') + }) + + // check session verify for with / without anti-csrf present + it('test session verify without anti-csrf present express', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', async (req, res) => { + try { + const sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: true }) + res.status(200).json({ success: false }) + } + catch (err) { + res.status(200).json({ + success: err.type === Session.Error.TRY_REFRESH_TOKEN, + }) + } + }) + + app.post('/session/verifyAntiCsrfFalse', async (req, res) => { + const sessionResponse = await Session.getSession(req, res, { antiCsrfCheck: false }) + res.status(200).json({ userId: sessionResponse.userId }) + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const response2 = await new Promise(resolve => + request(app) + .post('/session/verifyAntiCsrfFalse') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response2.body.userId, 'id1') + + const response = await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.body.success, true) + }) + + // check revoking session(s)** + it('test revoking express sessions', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + app.post('/usercreate', async (req, res) => { + await Session.createNewSession(req, res, 'someUniqueUserId', {}, {}) + res.status(200).send('') + }) + app.post('/session/revoke', async (req, res) => { + const session = await Session.getSession(req, res) + await session.revokeSession() + res.status(200).send('') + }) + + app.post('/session/revokeUserid', async (req, res) => { + const session = await Session.getSession(req, res) + await Session.revokeAllSessionsForUser(session.getUserId()) + res.status('200').send('') + }) + + // create an api call get sesssions from a userid "id1" that returns all the sessions for that userid + app.post('/session/getSessionsWithUserId1', async (req, res) => { + const sessionHandles = await Session.getAllSessionHandlesForUser('someUniqueUserId') + res.status(200).json(sessionHandles) + }) + + const response = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + + await new Promise(resolve => + request(app) + .post('/usercreate') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const userCreateResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/usercreate') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ), + ) + + await new Promise(resolve => + request(app) + .post('/session/revokeUserid') + .set('Cookie', [`sAccessToken=${userCreateResponse.accessToken}`]) + .set('anti-csrf', userCreateResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionHandleResponse = await new Promise(resolve => + request(app) + .post('/session/getSessionsWithUserId1') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert(sessionHandleResponse.body.length === 0) + }) + + // check manipulating session data + it('test manipulating session data with express', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + app.post('/updateSessionData', async (req, res) => { + const session = await Session.getSession(req, res) + await session.updateSessionData({ key: 'value' }) + res.status(200).send('') + }) + app.post('/getSessionData', async (req, res) => { + const session = await Session.getSession(req, res) + const sessionData = await session.getSessionData() + res.status(200).json(sessionData) + }) + + app.post('/updateSessionData2', async (req, res) => { + const session = await Session.getSession(req, res) + await session.updateSessionData(null) + res.status(200).send('') + }) + + app.post('/updateSessionDataInvalidSessionHandle', async (req, res) => { + res.status(200).json({ success: !(await Session.updateSessionData('InvalidHandle', { key: 'value3' })) }) + }) + + // create a new session + const response = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + // call the updateSessionData api to add session data + await new Promise(resolve => + request(app) + .post('/updateSessionData') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // call the getSessionData api to get session data + let response2 = await new Promise(resolve => + request(app) + .post('/getSessionData') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // check that the session data returned is valid + assert.strictEqual(response2.body.key, 'value') + + // change the value of the inserted session data + await new Promise(resolve => + request(app) + .post('/updateSessionData2') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + // retrieve the changed session data + response2 = await new Promise(resolve => + request(app) + .post('/getSessionData') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // check the value of the retrieved + assert.deepStrictEqual(response2.body, {}) + + // invalid session handle when updating the session data + const invalidSessionResponse = await new Promise(resolve => + request(app) + .post('/updateSessionDataInvalidSessionHandle') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(invalidSessionResponse.body.success, true) + }) + + // check manipulating jwt payload + it('test manipulating jwt payload with express', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'user1', {}, {}) + res.status(200).send('') + }) + app.post('/updateAccessTokenPayload', async (req, res) => { + const session = await Session.getSession(req, res) + const accessTokenBefore = session.getAccessToken() + await session.mergeIntoAccessTokenPayload({ key: 'value' }) + const accessTokenAfter = session.getAccessToken() + const statusCode = accessTokenBefore !== (accessTokenAfter && typeof accessTokenAfter === 'string') ? 200 : 500 + res.status(statusCode).send('') + }) + app.post('/auth/session/refresh', async (req, res) => { + await Session.refreshSession(req, res) + res.status(200).send('') + }) + app.post('/getAccessTokenPayload', async (req, res) => { + const session = await Session.getSession(req, res) + const jwtPayload = session.getAccessTokenPayload() + res.status(200).json(jwtPayload) + }) + + app.post('/updateAccessTokenPayload2', async (req, res) => { + const session = await Session.getSession(req, res) + try { + await session.mergeIntoAccessTokenPayload(undefined) + } + catch (error) { + console.log(error) + } + res.status(200).send('') + }) + + app.post('/updateAccessTokenPayloadInvalidSessionHandle', async (req, res) => { + res.status(200).json({ + success: !(await Session.updateAccessTokenPayload('InvalidHandle', { key: 'value3' })), + }) + }) + + // create a new session + const response = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + let frontendInfo = JSON.parse(new Buffer.from(response.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // call the updateAccessTokenPayload api to add jwt payload + const updatedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/updateAccessTokenPayload') + .set('Cookie', [`sAccessToken=${response.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + // call the getAccessTokenPayload api to get jwt payload + let response2 = await new Promise(resolve => + request(app) + .post('/getAccessTokenPayload') + .set('Cookie', [`sAccessToken=${updatedResponse.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + // check that the jwt payload returned is valid + assert.strictEqual(response2.body.key, 'value') + + // refresh session + response2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${response.refreshToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + frontendInfo = JSON.parse(new Buffer.from(response2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, { key: 'value' }) + + if (!response2) + throw new Error('accessToken is undefined') + // change the value of the inserted jwt payload + const updatedResponse2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/updateAccessTokenPayload2') + .set('Cookie', [`sAccessToken=${response2.accessToken}`]) + .set('anti-csrf', response2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + frontendInfo = JSON.parse(new Buffer.from(updatedResponse2.frontToken, 'base64').toString()) + assert(frontendInfo.uid === 'user1') + assert.deepStrictEqual(frontendInfo.up, {}) + + // retrieve the changed jwt payload + response2 = await new Promise(resolve => + request(app) + .post('/getAccessTokenPayload') + .set('Cookie', [`sAccessToken=${updatedResponse2.accessToken}`]) + .set('anti-csrf', response2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + // check the value of the retrieved + assert.deepStrictEqual(response2.body, {}) + // invalid session handle when updating the jwt payload + const invalidSessionResponse = await new Promise(resolve => + request(app) + .post('/updateAccessTokenPayloadInvalidSessionHandle') + .set('Cookie', [`sAccessToken=${updatedResponse2.accessToken}`]) + .set('anti-csrf', response.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(invalidSessionResponse.body.success, true) + }) + + // test with existing header params being there and that the lib appends to those and not overrides those + it('test that express appends to existing header params and does not override', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = express() + app.post('/create', async (req, res) => { + res.header('testHeader', 'testValue') + res.header('Access-Control-Expose-Headers', 'customValue') + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + // create a new session + + const response = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.headers.testheader, 'testValue') + assert.deepEqual(response.headers['access-control-expose-headers'], 'customValue, front-token, anti-csrf') + + // normal session headers + const extractInfo = extractInfoFromResponse(response) + assert(extractInfo.accessToken !== undefined) + assert(extractInfo.refreshToken != undefined) + assert(extractInfo.antiCsrf !== undefined) + }) + + // if anti-csrf is disabled from ST core, check that not having that in input to verify session is fine** + it('test that when anti-csrf is disabled from from ST core, not having to input in verify session is fine in express', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'NONE' })], + }) + + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', async (req, res) => { + const sessionResponse = await Session.getSession(req, res) + res.status(200).json({ userId: sessionResponse.userId }) + }) + app.post('/session/verifyAntiCsrfFalse', async (req, res) => { + const sessionResponse = await Session.getSession(req, res, false) + res.status(200).json({ userId: sessionResponse.userId }) + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + const res2 = await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(res2.body.userId, 'id1') + + const res3 = await new Promise(resolve => + request(app) + .post('/session/verifyAntiCsrfFalse') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(res3.body.userId, 'id1') + }) + + it('test that getSession does not clear cookies if a session does not exist in the first place', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.post('/session/verify', async (req, res) => { + try { + await Session.getSession(req, res) + } + catch (err) { + if (err.type === Session.Error.UNAUTHORISED) { + res.status(200).json({ success: true }) + return + } + } + res.status(200).json({ success: false }) + }) + + const res = await new Promise(resolve => + request(app) + .post('/session/verify') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.strictEqual(res.body.success, true) + + const cookies = extractInfoFromResponse(res) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, undefined) + assert.strictEqual(cookies.refreshToken, undefined) + assert.strictEqual(cookies.accessTokenExpiry, undefined) + assert.strictEqual(cookies.refreshTokenExpiry, undefined) + }) + + it('test that refreshSession does not clear cookies if a session does not exist in the first place', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + + const app = express() + + app.post('/auth/session/refresh', async (req, res) => { + try { + await Session.refreshSession(req, res) + } + catch (err) { + if (err.type === Session.Error.UNAUTHORISED) { + res.status(200).json({ success: true }) + return + } + } + res.status(200).json({ success: false }) + }) + + const res = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.strictEqual(res.body.success, true) + + const cookies = extractInfoFromResponse(res) + assert.strictEqual(cookies.antiCsrf, undefined) + assert.strictEqual(cookies.accessToken, undefined) + assert.strictEqual(cookies.refreshToken, undefined) + assert.strictEqual(cookies.accessTokenExpiry, undefined) + assert.strictEqual(cookies.refreshTokenExpiry, undefined) + }) + + it('test that when anti-csrf is enabled with custom header, and we don\'t provide that in verifySession, we get try refresh token', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_CUSTOM_HEADER' })], + }) + + const app = express() + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + const sessionResponse = req.session + res.status(200).json({ userId: sessionResponse.userId }) + }) + app.post('/session/verifyAntiCsrfFalse', verifySession({ antiCsrfCheck: false }), async (req, res) => { + const sessionResponse = req.session + res.status(200).json({ userId: sessionResponse.userId }) + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + { + const res2 = await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert.deepStrictEqual(res2.status, 401) + assert.deepStrictEqual(res2.text, '{"message":"try refresh token"}') + + const res3 = await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('rid', 'session') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.deepStrictEqual(res3.body.userId, 'id1') + } + + { + const res2 = await new Promise(resolve => + request(app) + .post('/session/verifyAntiCsrfFalse') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert.deepStrictEqual(res2.body.userId, 'id1') + + const res3 = await new Promise(resolve => + request(app) + .post('/session/verifyAntiCsrfFalse') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('rid', 'session') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.deepStrictEqual(res3.body.userId, 'id1') + } + }) + + it('test resfresh API when using CUSTOM HEADER anti-csrf', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_CUSTOM_HEADER' })], + }) + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + { + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + + assert.deepStrictEqual(res2.status, 401) + assert.deepStrictEqual(res2.text, '{"message":"unauthorised"}') + } + + { + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('rid', 'session') + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + + assert.deepStrictEqual(res2.status, 200) + } + }) + + it('test that init can be called post route and middleware declaration', async () => { + await startST() + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, 'id1', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + const sessionResponse = req.session + res.status(200).json({ userId: sessionResponse.userId }) + }) + app.post('/session/verifyAntiCsrfFalse', verifySession(false), async (req, res) => { + const sessionResponse = req.session + res.status(200).json({ userId: sessionResponse.userId }) + }) + + app.use(errorHandler()) + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_CUSTOM_HEADER' })], + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + { + const res2 = await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert.deepStrictEqual(res2.status, 401) + assert.deepStrictEqual(res2.text, '{"message":"try refresh token"}') + + const res3 = await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('rid', 'session') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.deepStrictEqual(res3.body.userId, 'id1') + } + }) + + it('test overriding of sessions functions', async () => { + await startST() + + let createNewSessionCalled = false + let getSessionCalled = false + let refreshSessionCalled = false + let session + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: (oI) => { + return { + ...oI, + createNewSession: async (input) => { + const response = await oI.createNewSession(input) + createNewSessionCalled = true + session = response + return response + }, + getSession: async (input) => { + const response = await oI.getSession(input) + getSessionCalled = true + session = response + return response + }, + refreshSession: async (input) => { + const response = await oI.refreshSession(input) + refreshSessionCalled = true + session = response + return response + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.post('/session/revoke', verifySession(), async (req, res) => { + const session = req.session + await session.revokeSession() + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(createNewSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert(res.accessToken !== undefined) + assert.strictEqual(session.getAccessToken(), decodeURIComponent(res.accessToken)) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + session = undefined + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + const verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500) + assert(verifyState3 === undefined) + + const res2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(refreshSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert(res2.accessToken !== undefined) + assert.strictEqual(session.getAccessToken(), decodeURIComponent(res2.accessToken)) + assert(res2.antiCsrf !== undefined) + assert(res2.refreshToken !== undefined) + session = undefined + + const res3 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + const verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY) + assert.strictEqual(getSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert(verifyState !== undefined) + assert(res3.accessToken !== undefined) + assert.strictEqual(session.getAccessToken(), decodeURIComponent(res3.accessToken)) + + ProcessState.getInstance().reset() + + await new Promise(resolve => + request(app) + .post('/session/verify') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000) + assert(verifyState2 === undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/session/revoke') + .set('Cookie', [`sAccessToken=${res3.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test overriding of sessions apis', async () => { + await startST() + + let signoutCalled = false + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: async (input) => { + const response = await oI.signOutPOST(input) + signoutCalled = true + return response + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + const sessionRevokedResponseExtracted = extractInfoFromResponse(sessionRevokedResponse) + assert.strictEqual(signoutCalled, true) + assert(sessionRevokedResponseExtracted.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(sessionRevokedResponseExtracted.accessToken === '') + assert(sessionRevokedResponseExtracted.refreshToken === '') + }) + + it('test overriding of sessions functions, error thrown', async () => { + await startST() + + let createNewSessionCalled = false + let session + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + functions: (oI) => { + return { + ...oI, + createNewSession: async (input) => { + const response = await oI.createNewSession(input) + createNewSessionCalled = true + session = response + throw { + error: 'create new session error', + } + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res, next) => { + try { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + } + catch (err) { + next(err) + } + }) + + app.use(errorHandler()) + + app.use((err, req, res, next) => { + res.json({ + customError: true, + ...err, + }) + }) + + const res = await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.strictEqual(createNewSessionCalled, true) + assert.notStrictEqual(session, undefined) + assert.deepStrictEqual(res, { customError: true, error: 'create new session error' }) + }) + + it('test overriding of sessions apis, error thrown', async () => { + await startST() + + let signoutCalled = false + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: async (input) => { + const response = await oI.signOutPOST(input) + signoutCalled = true + throw { + error: 'signout error', + } + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.post('/session/verify', verifySession(), async (req, res) => { + res.status(200).send('') + }) + + app.use(errorHandler()) + + app.use((err, req, res, next) => { + res.json({ + customError: true, + ...err, + }) + }) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + const sessionRevokedResponse = await new Promise(resolve => + request(app) + .post('/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.strictEqual(signoutCalled, true) + assert.deepStrictEqual(sessionRevokedResponse, { customError: true, error: 'signout error' }) + }) + + it('check that refresh doesn\'t clear cookies if missing anti csrf via custom header', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_CUSTOM_HEADER' })], + }) + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + { + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + + assert.deepStrictEqual(res2.status, 401) + assert.deepStrictEqual(res2.text, '{"message":"unauthorised"}') + const sessionRevokedResponseExtracted = extractInfoFromResponse(res2) + } + }) + + it('check that refresh doesn\'t clear cookies if missing anti csrf via token', async () => { + await startST() + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' })], + }) + const app = express() + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + { + const res2 = await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + + assert.deepStrictEqual(res2.status, 401) + assert.deepStrictEqual(res2.text, '{"message":"unauthorised"}') + } + }) + + it('test session error handler overriding', async () => { + await startST() + let testpass = false + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => 'cookie', + antiCsrf: 'VIA_TOKEN', + errorHandlers: { + onUnauthorised: async (message, request, response) => { + await new Promise(r => + setTimeout(() => { + testpass = true + r() + }, 5000), + ) + throw new Error('onUnauthorised error caught') + }, + }, + }), + ], + }) + + const app = express() + + app.post('/session/verify', async (req, res, next) => { + try { + await Session.getSession(req, res) + res.status(200).send('') + } + catch (err) { + next(err) + } + }) + + app.use(errorHandler()) + + app.use((err, req, res, next) => { + if (err.message === 'onUnauthorised error caught') { + res.status(403) + res.json({}) + } + }) + + const response = await new Promise(resolve => + request(app) + .post('/session/verify') + .expect(403) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.status, 403) + assert(testpass) + }) + + it('test revoking a session during refresh with revokeSession function', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + async refreshPOST(input) { + const session = await oI.refreshPOST(input) + await session.revokeSession() + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .set('st-auth-mode', 'cookie') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.notStrictEqual(res.accessToken, undefined) + assert.notStrictEqual(res.antiCsrf, undefined) + assert.notStrictEqual(res.refreshToken, undefined) + + const resp = await new Promise(resolve => + request(app) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(resp.status === 200) + + const res2 = extractInfoFromResponse(resp) + + assert.deepEqual(res2.accessToken, '') + assert.deepEqual(res2.refreshToken, '') + assert.deepEqual(res2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.deepEqual(res2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(res2.accessTokenDomain === undefined) + assert(res2.refreshTokenDomain === undefined) + assert.strictEqual(res2.frontToken, 'remove') + assert.strictEqual(res2.antiCsrf, undefined) + }) + + it('test revoking a session during refresh with revokeSession function and sending 401', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + async refreshPOST(input) { + const session = await oI.refreshPOST(input) + await session.revokeSession() + input.options.res.setStatusCode(401) + input.options.res.sendJSONResponse({}) + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .set('st-auth-mode', 'cookie') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + const resp = await new Promise(resolve => + request(app) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(resp.status === 401) + + const res2 = extractInfoFromResponse(resp) + + assert.deepEqual(res2.accessToken, '') + assert.deepEqual(res2.refreshToken, '') + assert.deepEqual(res2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.deepEqual(res2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(res2.accessTokenDomain === undefined) + assert(res2.refreshTokenDomain === undefined) + assert.strictEqual(res2.frontToken, 'remove') + assert.strictEqual(res2.antiCsrf, undefined) + }) + + it('test revoking a session during refresh with throwing unauthorised error', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + async refreshPOST(input) { + await oI.refreshPOST(input) + throw new Session.Error({ + message: 'unauthorised', + type: Session.Error.UNAUTHORISED, + clearTokens: true, + }) + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .set('st-auth-mode', 'cookie') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + const resp = await new Promise(resolve => + request(app) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`, `sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(resp.status === 401) + + const res2 = extractInfoFromResponse(resp) + + assert.strictEqual(res2.accessToken, '') + assert.strictEqual(res2.refreshToken, '') + assert.strictEqual(res2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(res2.accessTokenDomain, undefined) + assert.strictEqual(res2.refreshTokenDomain, undefined) + assert.strictEqual(res2.frontToken, 'remove') + assert.strictEqual(res2.antiCsrf, undefined) + }) + + it('test revoking a session during refresh fails if just sending 401', async () => { + await startST() + + SuperTokens.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + apiBasePath: '/', + }, + recipeList: [ + Session.init({ + antiCsrf: 'VIA_TOKEN', + override: { + apis: (oI) => { + return { + ...oI, + async refreshPOST(input) { + const session = await oI.refreshPOST(input) + input.options.res.setStatusCode(401) + input.options.res.sendJSONResponse({}) + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, '', {}, {}) + res.status(200).send('') + }) + + app.use(errorHandler()) + + const res = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/create') + .set('st-auth-mode', 'cookie') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(res.accessToken !== undefined) + assert(res.antiCsrf !== undefined) + assert(res.refreshToken !== undefined) + + const resp = await new Promise(resolve => + request(app) + .post('/session/refresh') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(resp.status === 401) + + const res2 = extractInfoFromResponse(resp) + + assert(res2.accessToken.length > 1) + assert(res2.antiCsrf.length > 1) + assert(res2.refreshToken.length > 1) + }) +}) diff --git a/test/thirdparty/authorisationUrlFeature.test.js b/test/thirdparty/authorisationUrlFeature.test.js deleted file mode 100644 index d7d521282..000000000 --- a/test/thirdparty/authorisationUrlFeature.test.js +++ /dev/null @@ -1,316 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -let ThirParty = require("../../lib/build/recipe/thirdparty"); -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`authorisationTest: ${printPath("[test/thirdparty/authorisationFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - params: { - scope: "test", - client_id: "supertokens", - dynamic: function dynamicParam(request) { - return request.query.dynamic; - }, - }, - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - - this.customProvider2 = { - id: "custom", - get: (recipe, authCode) => { - throw new Error("error from get function"); - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test that using development OAuth keys will use the development authorisation url", async function () { - await startST(); - - // testing with the google OAuth development key - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Google({ - clientId: "4398792-test-id", - clientSecret: "test-secret", - }), - ], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=google") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - - let url = new URL(response1.body.url); - assert.strictEqual(url.origin, "https://supertokens.io"); - - assert.strictEqual(url.pathname, "/dev/oauth/redirect-to-provider"); - }); - - it("test minimum config for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=custom&dynamic=example.com") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual( - response1.body.url, - "https://test.com/oauth/auth?scope=test&client_id=supertokens&dynamic=example.com" - ); - }); - - it("test provider get function throws error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider2], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=custom") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from get function" }); - }); - - it("test thirdparty provider doesn't exist", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=google") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual( - response1.body.message, - "The third party provider google seems to be missing from the backend configs." - ); - }); - - it("test invalid GET params for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual(response1.body.message, "Please provide the thirdPartyId as a GET param"); - }); -}); diff --git a/test/thirdparty/authorisationUrlFeature.test.ts b/test/thirdparty/authorisationUrlFeature.test.ts new file mode 100644 index 000000000..498028ff1 --- /dev/null +++ b/test/thirdparty/authorisationUrlFeature.test.ts @@ -0,0 +1,319 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import ThirParty, { TypeProvider } from 'supertokens-node/recipe/thirdparty' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`authorisationTest: ${printPath('[test/thirdparty/authorisationFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + let customProvider2: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + params: { + scope: 'test', + client_id: 'supertokens', + dynamic: function dynamicParam(request) { + return request.query.dynamic + }, + }, + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + + customProvider2 = { + id: 'custom', + get: (recipe, authCode) => { + throw new Error('error from get function') + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test that using development OAuth keys will use the development authorisation url', async () => { + await startST() + + // testing with the google OAuth development key + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Google({ + clientId: '4398792-test-id', + clientSecret: 'test-secret', + }), + ], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=google') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + + const url = new URL(response1.body.url) + assert.strictEqual(url.origin, 'https://supertokens.io') + + assert.strictEqual(url.pathname, '/dev/oauth/redirect-to-provider') + }) + + it('test minimum config for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=custom&dynamic=example.com') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual( + response1.body.url, + 'https://test.com/oauth/auth?scope=test&client_id=supertokens&dynamic=example.com', + ) + }) + + it('test provider get function throws error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider2], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=custom') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from get function' }) + }) + + it('test thirdparty provider doesn\'t exist', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=google') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual( + response1.body.message, + 'The third party provider google seems to be missing from the backend configs.', + ) + }) + + it('test invalid GET params for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual(response1.body.message, 'Please provide the thirdPartyId as a GET param') + }) +}) diff --git a/test/thirdparty/config.test.js b/test/thirdparty/config.test.js deleted file mode 100644 index 0a2845f11..000000000 --- a/test/thirdparty/config.test.js +++ /dev/null @@ -1,111 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, resetAll } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -let ThirParty = require("../../lib/build/recipe/thirdparty"); -let { middleware, errorHandler } = require("../../framework/express"); - -/** - * TODO - * - check with different inputs - */ -describe(`configTest: ${printPath("[test/thirdparty/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test config for thirdparty module, no provider passed", async function () { - await startST(); - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [], - }, - }), - ], - }); - assert(false); - } catch (err) { - assert.strictEqual( - err.message, - "thirdparty recipe requires atleast 1 provider to be passed in signInAndUpFeature.providers config" - ); - } - }); - - it("test minimum config for thirdparty module, custom provider", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "test.com/oauth/token", - }, - authorisationRedirect: { - url: "test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - }; - }, - }, - ], - }, - }), - ], - }); - }); -}); diff --git a/test/thirdparty/config.test.ts b/test/thirdparty/config.test.ts new file mode 100644 index 000000000..bf12a1211 --- /dev/null +++ b/test/thirdparty/config.test.ts @@ -0,0 +1,112 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +/** + * TODO + * - check with different inputs + */ +describe(`configTest: ${printPath('[test/thirdparty/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test config for thirdparty module, no provider passed', async () => { + await startST() + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [], + }, + }), + ], + }) + assert(false) + } + catch (err) { + assert.strictEqual( + err.message, + 'thirdparty recipe requires atleast 1 provider to be passed in signInAndUpFeature.providers config', + ) + } + }) + + it('test minimum config for thirdparty module, custom provider', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'test.com/oauth/token', + }, + authorisationRedirect: { + url: 'test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + } + }, + }, + ], + }, + }), + ], + }) + }) +}) diff --git a/test/thirdparty/getUsersByEmailFeature.test.js b/test/thirdparty/getUsersByEmailFeature.test.js deleted file mode 100644 index 3fffa0e94..000000000 --- a/test/thirdparty/getUsersByEmailFeature.test.js +++ /dev/null @@ -1,100 +0,0 @@ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -const { signInUp } = require("../../lib/build/recipe/thirdparty"); -const { getUsersByEmail } = require("../../lib/build/recipe/thirdparty"); -const { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`getUsersByEmail: ${printPath("[test/thirdparty/getUsersByEmailFeature.test.js]")}`, function () { - const MockThirdPartyProvider = { - id: "mock", - }; - - const MockThirdPartyProvider2 = { - id: "mock2", - }; - - const testSTConfig = { - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider], - }, - }), - ], - }; - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("invalid email yields empty users array", async function () { - await startST(); - STExpress.init(testSTConfig); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - // given there are no users - - // when - const thirdPartyUsers = await getUsersByEmail("john.doe@example.com"); - - // then - assert.strictEqual(thirdPartyUsers.length, 0); - }); - - it("valid email yields third party users", async function () { - await startST(); - STExpress.init({ - ...testSTConfig, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider, MockThirdPartyProvider2], - }, - }), - ], - }); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - await signInUp("mock", "thirdPartyJohnDoe", "john.doe@example.com"); - await signInUp("mock2", "thirdPartyDaveDoe", "john.doe@example.com"); - - const thirdPartyUsers = await getUsersByEmail("john.doe@example.com"); - - assert.strictEqual(thirdPartyUsers.length, 2); - - thirdPartyUsers.forEach((user) => { - assert.notStrictEqual(user.thirdParty.id, undefined); - assert.notStrictEqual(user.id, undefined); - assert.notStrictEqual(user.timeJoined, undefined); - assert.strictEqual(user.email, "john.doe@example.com"); - }); - }); -}); diff --git a/test/thirdparty/getUsersByEmailFeature.test.ts b/test/thirdparty/getUsersByEmailFeature.test.ts new file mode 100644 index 000000000..212a8ff17 --- /dev/null +++ b/test/thirdparty/getUsersByEmailFeature.test.ts @@ -0,0 +1,97 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import { TypeProvider, getUsersByEmail, signInUp } from 'supertokens-node/recipe/thirdparty' +import { maxVersion } from 'supertokens-node/utils' +import { Querier } from 'supertokens-node/querier' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getUsersByEmail: ${printPath('[test/thirdparty/getUsersByEmailFeature.test.ts]')}`, () => { + const MockThirdPartyProvider: TypeProvider = { + id: 'mock', + } + + const MockThirdPartyProvider2: TypeProvider = { + id: 'mock2', + } + + const testSTConfig = { + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [MockThirdPartyProvider], + }, + }), + ], + } + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('invalid email yields empty users array', async () => { + await startST() + STExpress.init(testSTConfig) + + const apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + // given there are no users + + // when + const thirdPartyUsers = await getUsersByEmail('john.doe@example.com') + + // then + assert.strictEqual(thirdPartyUsers.length, 0) + }) + + it('valid email yields third party users', async () => { + await startST() + STExpress.init({ + ...testSTConfig, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [MockThirdPartyProvider, MockThirdPartyProvider2], + }, + }), + ], + }) + + const apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + await signInUp('mock', 'thirdPartyJohnDoe', 'john.doe@example.com') + await signInUp('mock2', 'thirdPartyDaveDoe', 'john.doe@example.com') + + const thirdPartyUsers = await getUsersByEmail('john.doe@example.com') + + assert.strictEqual(thirdPartyUsers.length, 2) + + thirdPartyUsers.forEach((user) => { + assert.notStrictEqual(user.thirdParty.id, undefined) + assert.notStrictEqual(user.id, undefined) + assert.notStrictEqual(user.timeJoined, undefined) + assert.strictEqual(user.email, 'john.doe@example.com') + }) + }) +}) diff --git a/test/thirdparty/override.test.js b/test/thirdparty/override.test.js deleted file mode 100644 index 3272da69c..000000000 --- a/test/thirdparty/override.test.js +++ /dev/null @@ -1,516 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -const { Querier } = require("../../lib/build/querier"); -let ThirdParty = require("../../recipe/thirdparty"); -const express = require("express"); -const request = require("supertest"); -let nock = require("nock"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`overrideTest: ${printPath("[test/thirdparty/override.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("overriding functions tests", async function () { - await startST(); - let user = undefined; - let newUser = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - override: { - functions: (oI) => { - return { - ...oI, - signInUp: async (input) => { - let response = await oI.signInUp(input); - user = response.user; - newUser = response.createdNewUser; - return response; - }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdParty.getUserById(userId)); - }); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: signInResponse.user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); - }); - - it("overriding api tests", async function () { - await startST(); - let user = undefined; - let newUser = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - override: { - apis: (oI) => { - return { - ...oI, - signInUpPOST: async (input) => { - let response = await oI.signInUpPOST(input); - if (response.status === "OK") { - user = response.user; - newUser = response.createdNewUser; - } - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdParty.getUserById(userId)); - }); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); - }); - - it("overriding functions tests, throws error", async function () { - await startST(); - let user = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - override: { - functions: (oI) => { - return { - ...oI, - signInUp: async (input) => { - let response = await oI.signInUp(input); - user = response.user; - newUser = response.createdNewUser; - if (newUser) { - throw { - error: "signup error", - }; - } - throw { - error: "signin error", - }; - }, - getUserById: async (input) => { - await oI.getUserById(input); - throw { - error: "get user error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res, next) => { - try { - let userId = req.query.userId; - res.json(await ThirdParty.getUserById(userId)); - } catch (err) { - next(err); - } - }); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse, { error: "signup error", customError: true }); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(userByIdResponse, { error: "get user error", customError: true }); - }); - - it("overriding api tests, throws error", async function () { - await startST(); - let user = undefined; - let newUser = undefined; - let emailExists = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - override: { - apis: (oI) => { - return { - ...oI, - signInUpPOST: async (input) => { - let response = await oI.signInUpPOST(input); - user = response.user; - newUser = response.createdNewUser; - if (newUser) { - throw { - error: "signup error", - }; - } - throw { - error: "signin error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse, { error: "signup error", customError: true }); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - }); -}); diff --git a/test/thirdparty/override.test.ts b/test/thirdparty/override.test.ts new file mode 100644 index 000000000..eb450d72d --- /dev/null +++ b/test/thirdparty/override.test.ts @@ -0,0 +1,518 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import ThirdParty, { TypeProvider } from 'supertokens-node/recipe/thirdparty' +import express from 'express' +import request from 'supertest' +import nock from 'nock' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`overrideTest: ${printPath('[test/thirdparty/override.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('overriding functions tests', async () => { + await startST() + let user + let newUser + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + override: { + functions: (oI) => { + return { + ...oI, + signInUp: async (input) => { + const response = await oI.signInUp(input) + user = response.user + newUser = response.createdNewUser + return response + }, + getUserById: async (input) => { + const response = await oI.getUserById(input) + user = response + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await ThirdParty.getUserById(userId)) + }) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.deepStrictEqual(signUpResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, false) + assert.deepStrictEqual(signInResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: signInResponse.user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(userByIdResponse, user) + }) + + it('overriding api tests', async () => { + await startST() + let user + let newUser + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + override: { + apis: (oI) => { + return { + ...oI, + signInUpPOST: async (input) => { + const response = await oI.signInUpPOST(input) + if (response.status === 'OK') { + user = response.user + newUser = response.createdNewUser + } + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await ThirdParty.getUserById(userId)) + }) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.deepStrictEqual(signUpResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, false) + assert.deepStrictEqual(signInResponse.user, user) + }) + + it('overriding functions tests, throws error', async () => { + await startST() + let user + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + override: { + functions: (oI) => { + return { + ...oI, + signInUp: async (input) => { + const response = await oI.signInUp(input) + user = response.user + const newUser = response.createdNewUser + if (newUser) { + throw { + error: 'signup error', + } + } + throw { + error: 'signin error', + } + }, + getUserById: async (input) => { + await oI.getUserById(input) + throw { + error: 'get user error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res, next) => { + try { + const userId = req.query.userId + res.json(await ThirdParty.getUserById(userId)) + } + catch (err) { + next(err) + } + }) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse, { error: 'signup error', customError: true }) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(userByIdResponse, { error: 'get user error', customError: true }) + }) + + it('overriding api tests, throws error', async () => { + await startST() + let user + let newUser + const emailExists = undefined + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + override: { + apis: (oI) => { + return { + ...oI, + signInUpPOST: async (input) => { + const response = await oI.signInUpPOST(input) + user = response.user + const newUser = response.createdNewUser + if (newUser) { + throw { + error: 'signup error', + } + } + throw { + error: 'signin error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.deepStrictEqual(signUpResponse, { error: 'signup error', customError: true }) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.strictEqual(newUser, false) + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + }) +}) diff --git a/test/thirdparty/provider.test.js b/test/thirdparty/provider.test.js deleted file mode 100644 index 9f664d744..000000000 --- a/test/thirdparty/provider.test.js +++ /dev/null @@ -1,713 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -let ThirParty = require("../../lib/build/recipe/thirdparty"); -let { middleware, errorHandler } = require("../../framework/express"); - -const privateKey = `-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIP92u8DjfW31UDDudzWtcsiH/gJ5RpdgL6EV4FTuADZWoAcGBSuBBAAK\noUQDQgAEBorYK2YgYN1BDxVNtBgq8ZdoIR5m02kfJKFI/Vq1+uagvjjZVLpeUEgQ\n79ENddF5P8V8gRri+XzD2zNYpYXGNQ==\n-----END EC PRIVATE KEY-----`; - -/** - * TODO - * - Google - * - pass additional params, check they are present in authorisation url - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - * - Facebook - * - test minimum config - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - * - Github - * - test minimum config - * - pass additional params, check they are present in authorisation url - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - * - Apple - * - test minimum config - * - pass additional params, check they are present in authorisation url - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - */ -describe(`providerTest: ${printPath("[test/thirdparty/provider.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test minimum config for third party provider google", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Google({ - clientId: "test", - clientSecret: "test-secret", - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "google"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, "https://oauth2.googleapis.com/token"); - assert.strictEqual( - providerInfoGetResult.authorisationRedirect.url, - "https://accounts.google.com/o/oauth2/v2/auth" - ); - assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { - client_id: "test", - client_secret: "test-secret", - grant_type: "authorization_code", - }); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - scope: "https://www.googleapis.com/auth/userinfo.email", - }); - }); - - it("test passing additional params, check they are present in authorisation url for thirdparty provider google", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Google({ - clientId: "test", - clientSecret: "test-secret", - authorisationRedirect: { - params: { - key1: "value1", - key2: "value2", - }, - }, - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "google"); - let providerInfoGetResult = await providerInfo.get(); - - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - scope: "https://www.googleapis.com/auth/userinfo.email", - key1: "value1", - key2: "value2", - }); - }); - - it("test passing scopes in config for thirdparty provider google", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "google"); - let providerInfoGetResult = await providerInfo.get(); - - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - scope: "test-scope-1 test-scope-2", - }); - }); - - it("test minimum config for third party provider facebook", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Facebook({ - clientId, - clientSecret, - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "facebook"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual( - providerInfoGetResult.accessTokenAPI.url, - "https://graph.facebook.com/v9.0/oauth/access_token" - ); - assert.strictEqual( - providerInfoGetResult.authorisationRedirect.url, - "https://www.facebook.com/v9.0/dialog/oauth" - ); - assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { - client_id: clientId, - client_secret: clientSecret, - }); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - response_type: "code", - scope: "email", - }); - }); - - it("test passing scopes in config for third party provider facebook", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Facebook({ - clientId, - clientSecret, - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "facebook"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - response_type: "code", - scope: "test-scope-1 test-scope-2", - }); - }); - - it("test minimum config for third party provider github", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Github({ - clientId, - clientSecret, - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "github"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, "https://github.com/login/oauth/access_token"); - assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, "https://github.com/login/oauth/authorize"); - assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { - client_id: clientId, - client_secret: clientSecret, - }); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "read:user user:email", - }); - }); - - it("test additional params, check they are present in authorisation url for third party provider github", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Github({ - clientId, - clientSecret, - authorisationRedirect: { - params: { - key1: "value1", - key2: "value2", - }, - }, - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "github"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "read:user user:email", - key1: "value1", - key2: "value2", - }); - }); - - it("test passing scopes in config for third party provider github", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Github({ - clientId, - clientSecret, - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "github"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "test-scope-1 test-scope-2", - }); - }); - - it("test minimum config for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey, - teamId: "test-team-id", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Apple({ - clientId, - clientSecret, - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "apple"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, "https://appleid.apple.com/auth/token"); - assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, "https://appleid.apple.com/auth/authorize"); - - let accessTokenAPIParams = providerInfoGetResult.accessTokenAPI.params; - - assert(accessTokenAPIParams.client_id === clientId); - assert(accessTokenAPIParams.client_secret !== undefined); - assert(accessTokenAPIParams.grant_type === "authorization_code"); - - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "email", - response_mode: "form_post", - response_type: "code", - }); - }); - - it("test passing additional params, check they are present in authorisation url for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey, - teamId: "test-team-id", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Apple({ - clientId, - clientSecret, - authorisationRedirect: { - params: { - key1: "value1", - key2: "value2", - }, - }, - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "apple"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "email", - response_mode: "form_post", - response_type: "code", - key1: "value1", - key2: "value2", - }); - }); - - it("test passing scopes in config for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey, - teamId: "test-team-id", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Apple({ - clientId, - clientSecret, - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }, - }), - ], - }); - - let providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0]; - assert.strictEqual(providerInfo.id, "apple"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "test-scope-1 test-scope-2", - response_mode: "form_post", - response_type: "code", - }); - }); - - it("test passing invalid privateKey in config for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey: "invalidKey", - teamId: "test-team-id", - }; - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Apple({ - clientId, - clientSecret, - }), - ], - }, - }), - ], - }); - assert(false); - } catch (error) { - if (error.type !== ThirParty.Error.BAD_INPUT_ERROR) { - throw error; - } - } - }); - - it("test duplicate provider without any default", async function () { - await startST(); - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ThirParty.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }, - }), - ], - }); - throw new Error("should fail"); - } catch (err) { - assert( - err.message === - `The providers array has multiple entries for the same third party provider. Please mark one of them as the default one by using "isDefault: true".` - ); - } - }); - - it("test duplicate provider with both default", async function () { - await startST(); - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Google({ - isDefault: true, - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ThirParty.Google({ - isDefault: true, - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }, - }), - ], - }); - throw new Error("should fail"); - } catch (err) { - assert( - err.message === - `You have provided multiple third party providers that have the id: "google" and are marked as "isDefault: true". Please only mark one of them as isDefault.` - ); - } - }); - it("test duplicate provider with one default", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirParty.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ThirParty.Google({ - isDefault: true, - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }, - }), - ], - }); - }); -}); diff --git a/test/thirdparty/provider.test.ts b/test/thirdparty/provider.test.ts new file mode 100644 index 000000000..674dd53e0 --- /dev/null +++ b/test/thirdparty/provider.test.ts @@ -0,0 +1,716 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import ThirParty from 'supertokens-node/recipe/thirdparty' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +const privateKey = '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIP92u8DjfW31UDDudzWtcsiH/gJ5RpdgL6EV4FTuADZWoAcGBSuBBAAK\noUQDQgAEBorYK2YgYN1BDxVNtBgq8ZdoIR5m02kfJKFI/Vq1+uagvjjZVLpeUEgQ\n79ENddF5P8V8gRri+XzD2zNYpYXGNQ==\n-----END EC PRIVATE KEY-----' + +/** + * TODO + * - Google + * - pass additional params, check they are present in authorisation url + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + * - Facebook + * - test minimum config + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + * - Github + * - test minimum config + * - pass additional params, check they are present in authorisation url + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + * - Apple + * - test minimum config + * - pass additional params, check they are present in authorisation url + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + */ +describe(`providerTest: ${printPath('[test/thirdparty/provider.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test minimum config for third party provider google', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Google({ + clientId: 'test', + clientSecret: 'test-secret', + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'google') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, 'https://oauth2.googleapis.com/token') + assert.strictEqual( + providerInfoGetResult.authorisationRedirect.url, + 'https://accounts.google.com/o/oauth2/v2/auth', + ) + assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { + client_id: 'test', + client_secret: 'test-secret', + grant_type: 'authorization_code', + }) + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + scope: 'https://www.googleapis.com/auth/userinfo.email', + }) + }) + + it('test passing additional params, check they are present in authorisation url for thirdparty provider google', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Google({ + clientId: 'test', + clientSecret: 'test-secret', + authorisationRedirect: { + params: { + key1: 'value1', + key2: 'value2', + }, + }, + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'google') + const providerInfoGetResult = await providerInfo.get() + + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + scope: 'https://www.googleapis.com/auth/userinfo.email', + key1: 'value1', + key2: 'value2', + }) + }) + + it('test passing scopes in config for thirdparty provider google', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'google') + const providerInfoGetResult = await providerInfo.get() + + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + scope: 'test-scope-1 test-scope-2', + }) + }) + + it('test minimum config for third party provider facebook', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Facebook({ + clientId, + clientSecret, + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'facebook') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual( + providerInfoGetResult.accessTokenAPI.url, + 'https://graph.facebook.com/v9.0/oauth/access_token', + ) + assert.strictEqual( + providerInfoGetResult.authorisationRedirect.url, + 'https://www.facebook.com/v9.0/dialog/oauth', + ) + assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { + client_id: clientId, + client_secret: clientSecret, + }) + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + response_type: 'code', + scope: 'email', + }) + }) + + it('test passing scopes in config for third party provider facebook', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Facebook({ + clientId, + clientSecret, + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'facebook') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + response_type: 'code', + scope: 'test-scope-1 test-scope-2', + }) + }) + + it('test minimum config for third party provider github', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Github({ + clientId, + clientSecret, + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'github') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, 'https://github.com/login/oauth/access_token') + assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, 'https://github.com/login/oauth/authorize') + assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { + client_id: clientId, + client_secret: clientSecret, + }) + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'read:user user:email', + }) + }) + + it('test additional params, check they are present in authorisation url for third party provider github', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Github({ + clientId, + clientSecret, + authorisationRedirect: { + params: { + key1: 'value1', + key2: 'value2', + }, + }, + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'github') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'read:user user:email', + key1: 'value1', + key2: 'value2', + }) + }) + + it('test passing scopes in config for third party provider github', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Github({ + clientId, + clientSecret, + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'github') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'test-scope-1 test-scope-2', + }) + }) + + it('test minimum config for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey, + teamId: 'test-team-id', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Apple({ + clientId, + clientSecret, + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'apple') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, 'https://appleid.apple.com/auth/token') + assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, 'https://appleid.apple.com/auth/authorize') + + const accessTokenAPIParams = providerInfoGetResult.accessTokenAPI.params + + assert(accessTokenAPIParams.client_id === clientId) + assert(accessTokenAPIParams.client_secret !== undefined) + assert(accessTokenAPIParams.grant_type === 'authorization_code') + + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'email', + response_mode: 'form_post', + response_type: 'code', + }) + }) + + it('test passing additional params, check they are present in authorisation url for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey, + teamId: 'test-team-id', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Apple({ + clientId, + clientSecret, + authorisationRedirect: { + params: { + key1: 'value1', + key2: 'value2', + }, + }, + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'apple') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'email', + response_mode: 'form_post', + response_type: 'code', + key1: 'value1', + key2: 'value2', + }) + }) + + it('test passing scopes in config for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey, + teamId: 'test-team-id', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Apple({ + clientId, + clientSecret, + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }, + }), + ], + }) + + const providerInfo = ThirPartyRecipe.getInstanceOrThrowError().providers[0] + assert.strictEqual(providerInfo.id, 'apple') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'test-scope-1 test-scope-2', + response_mode: 'form_post', + response_type: 'code', + }) + }) + + it('test passing invalid privateKey in config for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey: 'invalidKey', + teamId: 'test-team-id', + } + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Apple({ + clientId, + clientSecret, + }), + ], + }, + }), + ], + }) + assert(false) + } + catch (error) { + if (error.type !== ThirParty.Error.BAD_INPUT_ERROR) + throw error + } + }) + + it('test duplicate provider without any default', async () => { + await startST() + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ThirParty.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }, + }), + ], + }) + throw new Error('should fail') + } + catch (err) { + assert( + err.message + === 'The providers array has multiple entries for the same third party provider. Please mark one of them as the default one by using "isDefault: true".', + ) + } + }) + + it('test duplicate provider with both default', async () => { + await startST() + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Google({ + isDefault: true, + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ThirParty.Google({ + isDefault: true, + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }, + }), + ], + }) + throw new Error('should fail') + } + catch (err) { + assert( + err.message + === 'You have provided multiple third party providers that have the id: "google" and are marked as "isDefault: true". Please only mark one of them as isDefault.', + ) + } + }) + it('test duplicate provider with one default', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirParty.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ThirParty.Google({ + isDefault: true, + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }, + }), + ], + }) + }) +}) diff --git a/test/thirdparty/signinupFeature.test.js b/test/thirdparty/signinupFeature.test.js deleted file mode 100644 index 895bc6fc9..000000000 --- a/test/thirdparty/signinupFeature.test.js +++ /dev/null @@ -1,1036 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -let ThirdParty = require("../../lib/build/recipe/thirdparty"); -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -const EmailVerification = require("../../recipe/emailverification"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signinupTest: ${printPath("[test/thirdparty/signinupFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider2 = { - id: "custom", - get: (recipe, authCode) => { - throw new Error("error from get function"); - }, - }; - this.customProvider3 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider4 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - throw new Error("error from getProfileInfo"); - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider5 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: false, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider6 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - if (authCodeResponse.access_token === undefined) { - return {}; - } - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test that disable api, the default signinup API does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdParty.init({ - override: { - apis: (oI) => { - return { - ...oI, - signInUpPOST: undefined, - }; - }, - }, - signInAndUpFeature: { - providers: [ - ThirdParty.Google({ - clientId: "test", - clientSecret: "test-secret", - }), - ], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "google", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.status, 404); - }); - - it("test minimum config without code for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider6], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - authCodeResponse: { - access_token: "saodiasjodai", - }, - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); - - let cookies1 = extractInfoFromResponse(response1); - assert.notStrictEqual(cookies1.accessToken, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.notStrictEqual(cookies1.antiCsrf, undefined); - assert.notStrictEqual(cookies1.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.strictEqual(cookies1.accessTokenDomain, undefined); - assert.strictEqual(cookies1.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies1.frontToken, "remove"); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - authCodeResponse: { - access_token: "saodiasjodai", - }, - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.notStrictEqual(response2, undefined); - assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); - - let cookies2 = extractInfoFromResponse(response2); - assert.notStrictEqual(cookies2.accessToken, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.notStrictEqual(cookies2.antiCsrf, undefined); - assert.notStrictEqual(cookies2.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.strictEqual(cookies2.accessTokenDomain, undefined); - assert.strictEqual(cookies2.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies2.frontToken, "remove"); - }); - - it("test missing code and authCodeResponse", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider6], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.status, 400); - }); - - it("test minimum config for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); - - let cookies1 = extractInfoFromResponse(response1); - assert.notStrictEqual(cookies1.accessToken, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.notStrictEqual(cookies1.antiCsrf, undefined); - assert.notStrictEqual(cookies1.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.strictEqual(cookies1.accessTokenDomain, undefined); - assert.strictEqual(cookies1.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies1.frontToken, "remove"); - - assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), true); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.notStrictEqual(response2, undefined); - assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); - - let cookies2 = extractInfoFromResponse(response2); - assert.notStrictEqual(cookies2.accessToken, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.notStrictEqual(cookies2.antiCsrf, undefined); - assert.notStrictEqual(cookies2.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.strictEqual(cookies2.accessTokenDomain, undefined); - assert.strictEqual(cookies2.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies2.frontToken, "remove"); - }); - - it("test minimum config for thirdparty module, email unverified", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider5], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); - - let cookies1 = extractInfoFromResponse(response1); - assert.notStrictEqual(cookies1.accessToken, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.notStrictEqual(cookies1.antiCsrf, undefined); - assert.notStrictEqual(cookies1.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.strictEqual(cookies1.accessTokenDomain, undefined); - assert.strictEqual(cookies1.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies1.frontToken, "remove"); - - assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), false); - }); - - it("test thirdparty provider doesn't exist", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "google", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual( - response1.body.message, - "The third party provider google seems to be missing from the backend configs." - ); - }); - - it("test provider get function throws error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider2], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from get function" }); - }); - - it("test email not returned in getProfileInfo function", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider3], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 200); - assert.strictEqual(response1.body.status, "NO_EMAIL_GIVEN_BY_PROVIDER"); - }); - - it("test error thrown from getProfileInfo function", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider4], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from getProfileInfo" }); - }); - - it("test invalid POST params for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({}) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual(response1.body.message, "Please provide the thirdPartyId in request body"); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response2.statusCode, 400); - assert.strictEqual( - response2.body.message, - "Please provide one of code or authCodeResponse in the request body" - ); - - let response3 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: false, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response3.statusCode, 400); - assert.strictEqual(response3.body.message, "Please provide the thirdPartyId in request body"); - - let response4 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: 12323, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response4.statusCode, 400); - assert.strictEqual(response4.body.message, "Please provide the thirdPartyId in request body"); - - let response5 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: true, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response5.statusCode, 400); - assert.strictEqual(response5.body.message, "Please make sure that the code in the request body is a string"); - - let response6 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: 32432432, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response6.statusCode, 400); - assert.strictEqual(response6.body.message, "Please make sure that the code in the request body is a string"); - - let response7 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response7.statusCode, 400); - assert.strictEqual(response7.body.message, "Please provide the redirectURI in request body"); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response8 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response8.statusCode, 200); - }); - - it("test getUserById when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdParty.getUserById("randomID"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdParty.getUserById(signUpUserInfo.id); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); - - it("test getUserByThirdPartyInfo when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdParty.getUserByThirdPartyInfo("custom", "user"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdParty.getUserByThirdPartyInfo("custom", "user"); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); -}); diff --git a/test/thirdparty/signinupFeature.test.ts b/test/thirdparty/signinupFeature.test.ts new file mode 100644 index 000000000..4d86da399 --- /dev/null +++ b/test/thirdparty/signinupFeature.test.ts @@ -0,0 +1,1044 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import express from 'express' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import ThirdParty, { TypeProvider } from 'supertokens-node/recipe/thirdparty' +import nock from 'nock' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { cleanST, extractInfoFromResponse, killAllST, printPath, setupST, startST } from '../utils' + +describe(`signinupTest: ${printPath('[test/thirdparty/signinupFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + let customProvider2: TypeProvider + let customProvider3: TypeProvider + let customProvider4: TypeProvider + let customProvider5: TypeProvider + let customProvider6: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider2 = { + id: 'custom', + get: (recipe, authCode) => { + throw new Error('error from get function') + }, + } + customProvider3 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider4 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + throw new Error('error from getProfileInfo') + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider5 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: false, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider6 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + if (authCodeResponse.access_token === undefined) + return {} + + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test that disable api, the default signinup API does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdParty.init({ + override: { + apis: (oI) => { + return { + ...oI, + signInUpPOST: undefined, + } + }, + }, + signInAndUpFeature: { + providers: [ + ThirdParty.Google({ + clientId: 'test', + clientSecret: 'test-secret', + }), + ], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'google', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.status, 404) + }) + + it('test minimum config without code for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider6], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + authCodeResponse: { + access_token: 'saodiasjodai', + }, + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.createdNewUser, true) + assert.strictEqual(response1.body.user.thirdParty.id, 'custom') + assert.strictEqual(response1.body.user.thirdParty.userId, 'user') + assert.strictEqual(response1.body.user.email, 'email@test.com') + + const cookies1 = extractInfoFromResponse(response1) + assert.notStrictEqual(cookies1.accessToken, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.notStrictEqual(cookies1.antiCsrf, undefined) + assert.notStrictEqual(cookies1.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.strictEqual(cookies1.accessTokenDomain, undefined) + assert.strictEqual(cookies1.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies1.frontToken, 'remove') + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + authCodeResponse: { + access_token: 'saodiasjodai', + }, + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.notStrictEqual(response2, undefined) + assert.strictEqual(response2.body.status, 'OK') + assert.strictEqual(response2.body.createdNewUser, false) + assert.strictEqual(response2.body.user.thirdParty.id, 'custom') + assert.strictEqual(response2.body.user.thirdParty.userId, 'user') + assert.strictEqual(response2.body.user.email, 'email@test.com') + + const cookies2 = extractInfoFromResponse(response2) + assert.notStrictEqual(cookies2.accessToken, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.notStrictEqual(cookies2.antiCsrf, undefined) + assert.notStrictEqual(cookies2.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.strictEqual(cookies2.accessTokenDomain, undefined) + assert.strictEqual(cookies2.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies2.frontToken, 'remove') + }) + + it('test missing code and authCodeResponse', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider6], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.status, 400) + }) + + it('test minimum config for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.createdNewUser, true) + assert.strictEqual(response1.body.user.thirdParty.id, 'custom') + assert.strictEqual(response1.body.user.thirdParty.userId, 'user') + assert.strictEqual(response1.body.user.email, 'email@test.com') + + const cookies1 = extractInfoFromResponse(response1) + assert.notStrictEqual(cookies1.accessToken, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.notStrictEqual(cookies1.antiCsrf, undefined) + assert.notStrictEqual(cookies1.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.strictEqual(cookies1.accessTokenDomain, undefined) + assert.strictEqual(cookies1.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies1.frontToken, 'remove') + + assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), true) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.notStrictEqual(response2, undefined) + assert.strictEqual(response2.body.status, 'OK') + assert.strictEqual(response2.body.createdNewUser, false) + assert.strictEqual(response2.body.user.thirdParty.id, 'custom') + assert.strictEqual(response2.body.user.thirdParty.userId, 'user') + assert.strictEqual(response2.body.user.email, 'email@test.com') + + const cookies2 = extractInfoFromResponse(response2) + assert.notStrictEqual(cookies2.accessToken, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.notStrictEqual(cookies2.antiCsrf, undefined) + assert.notStrictEqual(cookies2.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.strictEqual(cookies2.accessTokenDomain, undefined) + assert.strictEqual(cookies2.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies2.frontToken, 'remove') + }) + + it('test minimum config for thirdparty module, email unverified', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider5], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.createdNewUser, true) + assert.strictEqual(response1.body.user.thirdParty.id, 'custom') + assert.strictEqual(response1.body.user.thirdParty.userId, 'user') + assert.strictEqual(response1.body.user.email, 'email@test.com') + + const cookies1 = extractInfoFromResponse(response1) + assert.notStrictEqual(cookies1.accessToken, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.notStrictEqual(cookies1.antiCsrf, undefined) + assert.notStrictEqual(cookies1.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.strictEqual(cookies1.accessTokenDomain, undefined) + assert.strictEqual(cookies1.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies1.frontToken, 'remove') + + assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), false) + }) + + it('test thirdparty provider doesn\'t exist', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'google', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual( + response1.body.message, + 'The third party provider google seems to be missing from the backend configs.', + ) + }) + + it('test provider get function throws error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider2], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from get function' }) + }) + + it('test email not returned in getProfileInfo function', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider3], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 200) + assert.strictEqual(response1.body.status, 'NO_EMAIL_GIVEN_BY_PROVIDER') + }) + + it('test error thrown from getProfileInfo function', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider4], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from getProfileInfo' }) + }) + + it('test invalid POST params for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({}) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual(response1.body.message, 'Please provide the thirdPartyId in request body') + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response2.statusCode, 400) + assert.strictEqual( + response2.body.message, + 'Please provide one of code or authCodeResponse in the request body', + ) + + const response3 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: false, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response3.statusCode, 400) + assert.strictEqual(response3.body.message, 'Please provide the thirdPartyId in request body') + + const response4 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 12323, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response4.statusCode, 400) + assert.strictEqual(response4.body.message, 'Please provide the thirdPartyId in request body') + + const response5 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: true, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response5.statusCode, 400) + assert.strictEqual(response5.body.message, 'Please make sure that the code in the request body is a string') + + const response6 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 32432432, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response6.statusCode, 400) + assert.strictEqual(response6.body.message, 'Please make sure that the code in the request body is a string') + + const response7 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response7.statusCode, 400) + assert.strictEqual(response7.body.message, 'Please provide the redirectURI in request body') + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response8 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response8.statusCode, 200) + }) + + it('test getUserById when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const thirdPartyRecipe = ThirdPartyRecipe.getInstanceOrThrowError() + + assert.strictEqual(await ThirdParty.getUserById('randomID'), undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + const userInfo = await ThirdParty.getUserById(signUpUserInfo.id) + + assert.strictEqual(userInfo.email, signUpUserInfo.email) + assert.strictEqual(userInfo.id, signUpUserInfo.id) + }) + + it('test getUserByThirdPartyInfo when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const thirdPartyRecipe = ThirdPartyRecipe.getInstanceOrThrowError() + + assert.strictEqual(await ThirdParty.getUserByThirdPartyInfo('custom', 'user'), undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + const userInfo = await ThirdParty.getUserByThirdPartyInfo('custom', 'user') + + assert.strictEqual(userInfo.email, signUpUserInfo.email) + assert.strictEqual(userInfo.id, signUpUserInfo.id) + }) +}) diff --git a/test/thirdparty/signoutFeature.test.js b/test/thirdparty/signoutFeature.test.js deleted file mode 100644 index 1c235af41..000000000 --- a/test/thirdparty/signoutFeature.test.js +++ /dev/null @@ -1,363 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, -} = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signoutTest: ${printPath("[test/thirdparty/signoutFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test the default route and it should revoke the session", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.statusCode, 200); - - let res = extractInfoFromResponse(response1); - - let response2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - assert.strictEqual(response2.antiCsrf, undefined); - assert.strictEqual(response2.accessToken, ""); - assert.strictEqual(response2.refreshToken, ""); - assert.strictEqual(response2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.accessTokenDomain, undefined); - assert.strictEqual(response2.refreshTokenDomain, undefined); - assert.strictEqual(response2.frontToken, "remove"); - }); - - it("test that disabling default route and calling the API returns 404", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "thirdparty") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 404); - }); - - it("test that calling the API without a session should return OK", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.body.status, "OK"); - assert.strictEqual(response.status, 200); - assert.strictEqual(response.header["set-cookie"], undefined); - }); - - it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.statusCode, 200); - - let res = extractInfoFromResponse(response1); - - await new Promise((r) => setTimeout(r, 5000)); - - let signOutResponse = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(signOutResponse.status, 401); - assert.strictEqual(signOutResponse.body.message, "try refresh token"); - - let refreshedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .expect(200) - .set("rid", "session") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - signOutResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + refreshedResponse.accessToken]) - .set("anti-csrf", refreshedResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(signOutResponse.antiCsrf, undefined); - assert.strictEqual(signOutResponse.accessToken, ""); - assert.strictEqual(signOutResponse.refreshToken, ""); - assert.strictEqual(signOutResponse.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(signOutResponse.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(signOutResponse.accessTokenDomain, undefined); - assert.strictEqual(signOutResponse.refreshTokenDomain, undefined); - }); -}); diff --git a/test/thirdparty/signoutFeature.test.ts b/test/thirdparty/signoutFeature.test.ts new file mode 100644 index 000000000..22676f7df --- /dev/null +++ b/test/thirdparty/signoutFeature.test.ts @@ -0,0 +1,367 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import nock from 'nock' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { TypeProvider } from 'supertokens-node/recipe/thirdparty' +import { + cleanST, + extractInfoFromResponse, + killAllST, + printPath, + setKeyValueInConfig, + setupST, + startST, +} from '../utils' + +describe(`signoutTest: ${printPath('[test/thirdparty/signoutFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test the default route and it should revoke the session', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.statusCode, 200) + + const res = extractInfoFromResponse(response1) + + const response2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + assert.strictEqual(response2.antiCsrf, undefined) + assert.strictEqual(response2.accessToken, '') + assert.strictEqual(response2.refreshToken, '') + assert.strictEqual(response2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.accessTokenDomain, undefined) + assert.strictEqual(response2.refreshTokenDomain, undefined) + assert.strictEqual(response2.frontToken, 'remove') + }) + + it('test that disabling default route and calling the API returns 404', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'thirdparty') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 404) + }) + + it('test that calling the API without a session should return OK', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.body.status, 'OK') + assert.strictEqual(response.status, 200) + assert.strictEqual(response.header['set-cookie'], undefined) + }) + + it('test that signout API reutrns try refresh token, refresh session and signout should return OK', async () => { + await setKeyValueInConfig('access_token_validity', 2) + + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.statusCode, 200) + + const res = extractInfoFromResponse(response1) + + await new Promise(r => setTimeout(r, 5000)) + + let signOutResponse = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(signOutResponse.status, 401) + assert.strictEqual(signOutResponse.body.message, 'try refresh token') + + const refreshedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .expect(200) + .set('rid', 'session') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + signOutResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${refreshedResponse.accessToken}`]) + .set('anti-csrf', refreshedResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(signOutResponse.antiCsrf, undefined) + assert.strictEqual(signOutResponse.accessToken, '') + assert.strictEqual(signOutResponse.refreshToken, '') + assert.strictEqual(signOutResponse.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(signOutResponse.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(signOutResponse.accessTokenDomain, undefined) + assert.strictEqual(signOutResponse.refreshTokenDomain, undefined) + }) +}) diff --git a/test/thirdparty/users.test.js b/test/thirdparty/users.test.js deleted file mode 100644 index c715bdbca..000000000 --- a/test/thirdparty/users.test.js +++ /dev/null @@ -1,249 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, signInUPCustomRequest } = require("../utils"); -const { getUserCount, getUsersNewestFirst, getUsersOldestFirst } = require("../../lib/build/"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let ThirPartyRecipe = require("../../lib/build/recipe/thirdparty/recipe").default; -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`usersTest: ${printPath("[test/thirdparty/users.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: authCodeResponse.id, - email: { - id: authCodeResponse.email, - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test getUsersOldestFirst", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signInUPCustomRequest(app, "test@gmail.com", "testPass0"); - await signInUPCustomRequest(app, "test1@gmail.com", "testPass1"); - await signInUPCustomRequest(app, "test2@gmail.com", "testPass2"); - await signInUPCustomRequest(app, "test3@gmail.com", "testPass3"); - await signInUPCustomRequest(app, "test4@gmail.com", "testPass4"); - - let users = await getUsersOldestFirst(); - assert.strictEqual(users.users.length, 5); - assert.strictEqual(users.nextPaginationToken, undefined); - - users = await getUsersOldestFirst({ limit: 1 }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersOldestFirst({ limit: 1, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test1@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersOldestFirst({ limit: 5, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 3); - assert.strictEqual(users.nextPaginationToken, undefined); - - try { - await getUsersOldestFirst({ limit: 10, paginationToken: "invalid-pagination-token" }); - assert(false); - } catch (err) { - if (!err.message.includes("invalid pagination token")) { - throw err; - } - } - - try { - await getUsersOldestFirst({ limit: -1 }); - assert(false); - } catch (err) { - if (!err.message.includes("limit must a positive integer with min value 1")) { - throw err; - } - } - }); - - it("test getUsersNewestFirst", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signInUPCustomRequest(app, "test@gmail.com", "testPass0"); - await signInUPCustomRequest(app, "test1@gmail.com", "testPass1"); - await signInUPCustomRequest(app, "test2@gmail.com", "testPass2"); - await signInUPCustomRequest(app, "test3@gmail.com", "testPass3"); - await signInUPCustomRequest(app, "test4@gmail.com", "testPass4"); - - let users = await getUsersNewestFirst(); - assert.strictEqual(users.users.length, 5); - assert.strictEqual(users.nextPaginationToken, undefined); - - users = await getUsersNewestFirst({ limit: 1 }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test4@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersNewestFirst({ limit: 1, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test3@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersNewestFirst({ limit: 5, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 3); - assert.strictEqual(users.nextPaginationToken, undefined); - - try { - await getUsersOldestFirst({ limit: 10, paginationToken: "invalid-pagination-token" }); - assert(false); - } catch (err) { - if (!err.message.includes("invalid pagination token")) { - throw err; - } - } - - try { - await getUsersOldestFirst({ limit: -1 }); - assert(false); - } catch (err) { - if (!err.message.includes("limit must a positive integer with min value 1")) { - throw err; - } - } - }); - - it("test getUserCount", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirPartyRecipe.init({ - signInAndUpFeature: { - providers: [this.customProvider1], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let userCount = await getUserCount(); - assert.strictEqual(userCount, 0); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signInUPCustomRequest(app, "test@gmail.com", "testPass0"); - userCount = await getUserCount(); - assert.strictEqual(userCount, 1); - - await signInUPCustomRequest(app, "test1@gmail.com", "testPass1"); - await signInUPCustomRequest(app, "test2@gmail.com", "testPass2"); - await signInUPCustomRequest(app, "test3@gmail.com", "testPass3"); - await signInUPCustomRequest(app, "test4@gmail.com", "testPass4"); - - userCount = await getUserCount(); - assert.strictEqual(userCount, 5); - }); -}); diff --git a/test/thirdparty/users.test.ts b/test/thirdparty/users.test.ts new file mode 100644 index 000000000..25abc5dcd --- /dev/null +++ b/test/thirdparty/users.test.ts @@ -0,0 +1,251 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress, { getUserCount, getUsersNewestFirst, getUsersOldestFirst } from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import Session from 'supertokens-node/recipe/session' +import ThirPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { TypeProvider } from 'supertokens-node/recipe/thirdparty' +import express from 'express' +import { cleanST, killAllST, printPath, setupST, signInUPCustomRequest, startST } from '../utils' + +describe(`usersTest: ${printPath('[test/thirdparty/users.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: authCodeResponse.id, + email: { + id: authCodeResponse.email, + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test getUsersOldestFirst', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signInUPCustomRequest(app, 'test@gmail.com', 'testPass0') + await signInUPCustomRequest(app, 'test1@gmail.com', 'testPass1') + await signInUPCustomRequest(app, 'test2@gmail.com', 'testPass2') + await signInUPCustomRequest(app, 'test3@gmail.com', 'testPass3') + await signInUPCustomRequest(app, 'test4@gmail.com', 'testPass4') + + let users = await getUsersOldestFirst() + assert.strictEqual(users.users.length, 5) + assert.strictEqual(users.nextPaginationToken, undefined) + + users = await getUsersOldestFirst({ limit: 1 }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersOldestFirst({ limit: 1, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test1@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersOldestFirst({ limit: 5, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 3) + assert.strictEqual(users.nextPaginationToken, undefined) + + try { + await getUsersOldestFirst({ limit: 10, paginationToken: 'invalid-pagination-token' }) + assert(false) + } + catch (err) { + if (!err.message.includes('invalid pagination token')) + throw err + } + + try { + await getUsersOldestFirst({ limit: -1 }) + assert(false) + } + catch (err) { + if (!err.message.includes('limit must a positive integer with min value 1')) + throw err + } + }) + + it('test getUsersNewestFirst', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signInUPCustomRequest(app, 'test@gmail.com', 'testPass0') + await signInUPCustomRequest(app, 'test1@gmail.com', 'testPass1') + await signInUPCustomRequest(app, 'test2@gmail.com', 'testPass2') + await signInUPCustomRequest(app, 'test3@gmail.com', 'testPass3') + await signInUPCustomRequest(app, 'test4@gmail.com', 'testPass4') + + let users = await getUsersNewestFirst() + assert.strictEqual(users.users.length, 5) + assert.strictEqual(users.nextPaginationToken, undefined) + + users = await getUsersNewestFirst({ limit: 1 }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test4@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersNewestFirst({ limit: 1, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test3@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersNewestFirst({ limit: 5, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 3) + assert.strictEqual(users.nextPaginationToken, undefined) + + try { + await getUsersOldestFirst({ limit: 10, paginationToken: 'invalid-pagination-token' }) + assert(false) + } + catch (err) { + if (!err.message.includes('invalid pagination token')) + throw err + } + + try { + await getUsersOldestFirst({ limit: -1 }) + assert(false) + } + catch (err) { + if (!err.message.includes('limit must a positive integer with min value 1')) + throw err + } + }) + + it('test getUserCount', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirPartyRecipe.init({ + signInAndUpFeature: { + providers: [customProvider1], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + let userCount = await getUserCount() + assert.strictEqual(userCount, 0) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signInUPCustomRequest(app, 'test@gmail.com', 'testPass0') + userCount = await getUserCount() + assert.strictEqual(userCount, 1) + + await signInUPCustomRequest(app, 'test1@gmail.com', 'testPass1') + await signInUPCustomRequest(app, 'test2@gmail.com', 'testPass2') + await signInUPCustomRequest(app, 'test3@gmail.com', 'testPass3') + await signInUPCustomRequest(app, 'test4@gmail.com', 'testPass4') + + userCount = await getUserCount() + assert.strictEqual(userCount, 5) + }) +}) diff --git a/test/thirdpartyemailpassword/authorisationUrlFeature.test.js b/test/thirdpartyemailpassword/authorisationUrlFeature.test.js deleted file mode 100644 index 3e5c8ad17..000000000 --- a/test/thirdpartyemailpassword/authorisationUrlFeature.test.js +++ /dev/null @@ -1,211 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPasswordRecipe = require("../../lib/build/recipe/thirdpartyemailpassword/recipe").default; -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authorisationFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - params: { - scope: "test", - client_id: "supertokens", - }, - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - - this.customProvider2 = { - id: "custom", - get: (recipe, authCode) => { - throw new Error("error from get function"); - }, - getClientId: () => { - return "supertokens"; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test minimum config for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyEmailPasswordRecipe.init({ - providers: [this.customProvider1], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=custom") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.url, "https://test.com/oauth/auth?scope=test&client_id=supertokens"); - }); - - // testing 500 error thrown from sub-recipe - it("test provider get function throws error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyEmailPasswordRecipe.init({ - providers: [this.customProvider2], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=custom") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from get function" }); - }); - - // testing 4xx error correctly thrown from sub-recipe - it("test thirdparty provider doesn't exist", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyEmailPasswordRecipe.init({ - providers: [this.customProvider1], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=google") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual( - response1.body.message, - "The third party provider google seems to be missing from the backend configs." - ); - }); -}); diff --git a/test/thirdpartyemailpassword/authorisationUrlFeature.test.ts b/test/thirdpartyemailpassword/authorisationUrlFeature.test.ts new file mode 100644 index 000000000..d4909f292 --- /dev/null +++ b/test/thirdpartyemailpassword/authorisationUrlFeature.test.ts @@ -0,0 +1,215 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyEmailPasswordRecipe from 'supertokens-node/recipe/thirdpartyemailpassword/recipe' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`authorisationTest: ${printPath('[test/thirdpartyemailpassword/authorisationFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + let customProvider2: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + params: { + scope: 'test', + client_id: 'supertokens', + }, + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + + customProvider2 = { + id: 'custom', + get: (recipe, authCode) => { + throw new Error('error from get function') + }, + getClientId: () => { + return 'supertokens' + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test minimum config for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyEmailPasswordRecipe.init({ + providers: [customProvider1], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=custom') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.url, 'https://test.com/oauth/auth?scope=test&client_id=supertokens') + }) + + // testing 500 error thrown from sub-recipe + it('test provider get function throws error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyEmailPasswordRecipe.init({ + providers: [customProvider2], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=custom') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from get function' }) + }) + + // testing 4xx error correctly thrown from sub-recipe + it('test thirdparty provider doesn\'t exist', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyEmailPasswordRecipe.init({ + providers: [customProvider1], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=google') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual( + response1.body.message, + 'The third party provider google seems to be missing from the backend configs.', + ) + }) +}) diff --git a/test/thirdpartyemailpassword/config.test.js b/test/thirdpartyemailpassword/config.test.js deleted file mode 100644 index c336b237b..000000000 --- a/test/thirdpartyemailpassword/config.test.js +++ /dev/null @@ -1,151 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { printPath, setupST, startST, stopST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -let ThirdPartyEmailPasswordRecipe = require("../../lib/build/recipe/thirdpartyemailpassword/recipe").default; - -describe(`configTest: ${printPath("[test/thirdpartyemailpassword/config.test.js]")}`, function () { - before(function () { - this.customProvider = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test default config for thirdpartyemailpassword module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init()], - }); - - let thirdpartyemailpassword = await ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - - assert.strictEqual(thirdpartyemailpassword.thirdPartyRecipe, undefined); - - let emailpassword = thirdpartyemailpassword.emailPasswordRecipe; - - let signUpFeature = emailpassword.config.signUpFeature; - assert.strictEqual(signUpFeature.formFields.length, 2); - assert.strictEqual(signUpFeature.formFields.filter((f) => f.id === "email")[0].optional, false); - assert.strictEqual(signUpFeature.formFields.filter((f) => f.id === "password")[0].optional, false); - assert.notStrictEqual(signUpFeature.formFields.filter((f) => f.id === "email")[0].validate, undefined); - assert.notStrictEqual(signUpFeature.formFields.filter((f) => f.id === "password")[0].validate, undefined); - - let signInFeature = emailpassword.config.signInFeature; - assert.strictEqual(signInFeature.formFields.length, 2); - assert.strictEqual(signInFeature.formFields.filter((f) => f.id === "email")[0].optional, false); - assert.strictEqual(signInFeature.formFields.filter((f) => f.id === "password")[0].optional, false); - assert.notStrictEqual(signInFeature.formFields.filter((f) => f.id === "email")[0].validate, undefined); - assert.notStrictEqual(signInFeature.formFields.filter((f) => f.id === "password")[0].validate, undefined); - - let resetPasswordUsingTokenFeature = emailpassword.config.resetPasswordUsingTokenFeature; - - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm.length, 1); - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm[0].id, "email"); - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm.length, 1); - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm[0].id, "password"); - }); - - it("test config for thirdpartyemailpassword module, with provider", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - providers: [this.customProvider], - }), - ], - }); - - let thirdpartyemailpassword = await ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - let thirdParty = thirdpartyemailpassword.thirdPartyRecipe; - - assert.notStrictEqual(thirdParty, undefined); - let emailVerificationFeatureTP = thirdpartyemailpassword.thirdPartyRecipe.config.emailVerificationFeature; - - let emailpassword = thirdpartyemailpassword.emailPasswordRecipe; - - let signUpFeature = emailpassword.config.signUpFeature; - assert.strictEqual(signUpFeature.formFields.length, 2); - assert.strictEqual(signUpFeature.formFields.filter((f) => f.id === "email")[0].optional, false); - assert.strictEqual(signUpFeature.formFields.filter((f) => f.id === "password")[0].optional, false); - assert.notStrictEqual(signUpFeature.formFields.filter((f) => f.id === "email")[0].validate, undefined); - assert.notStrictEqual(signUpFeature.formFields.filter((f) => f.id === "password")[0].validate, undefined); - - let signInFeature = emailpassword.config.signInFeature; - assert.strictEqual(signInFeature.formFields.length, 2); - assert.strictEqual(signInFeature.formFields.filter((f) => f.id === "email")[0].optional, false); - assert.strictEqual(signInFeature.formFields.filter((f) => f.id === "password")[0].optional, false); - assert.notStrictEqual(signInFeature.formFields.filter((f) => f.id === "email")[0].validate, undefined); - assert.notStrictEqual(signInFeature.formFields.filter((f) => f.id === "password")[0].validate, undefined); - - let resetPasswordUsingTokenFeature = emailpassword.config.resetPasswordUsingTokenFeature; - - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm.length, 1); - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm[0].id, "email"); - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm.length, 1); - assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm[0].id, "password"); - }); -}); diff --git a/test/thirdpartyemailpassword/config.test.ts b/test/thirdpartyemailpassword/config.test.ts new file mode 100644 index 000000000..89e0cffff --- /dev/null +++ b/test/thirdpartyemailpassword/config.test.ts @@ -0,0 +1,153 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import ProcessState from 'supertokens-node/processState' +import ThirdPartyEmailPassword, { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword' +import ThirdPartyEmailPasswordRecipe from 'supertokens-node/recipe/thirdpartyemailpassword/recipe' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`configTest: ${printPath('[test/thirdpartyemailpassword/config.test.ts]')}`, () => { + let customProvider: TypeProvider + beforeAll(() => { + customProvider = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test default config for thirdpartyemailpassword module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init()], + }) + + const thirdpartyemailpassword = await ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + + assert.strictEqual(thirdpartyemailpassword.thirdPartyRecipe, undefined) + + const emailpassword = thirdpartyemailpassword.emailPasswordRecipe + + const signUpFeature = emailpassword.config.signUpFeature + assert.strictEqual(signUpFeature.formFields.length, 2) + assert.strictEqual(signUpFeature.formFields.filter(f => f.id === 'email')[0].optional, false) + assert.strictEqual(signUpFeature.formFields.filter(f => f.id === 'password')[0].optional, false) + assert.notStrictEqual(signUpFeature.formFields.filter(f => f.id === 'email')[0].validate, undefined) + assert.notStrictEqual(signUpFeature.formFields.filter(f => f.id === 'password')[0].validate, undefined) + + const signInFeature = emailpassword.config.signInFeature + assert.strictEqual(signInFeature.formFields.length, 2) + assert.strictEqual(signInFeature.formFields.filter(f => f.id === 'email')[0].optional, false) + assert.strictEqual(signInFeature.formFields.filter(f => f.id === 'password')[0].optional, false) + assert.notStrictEqual(signInFeature.formFields.filter(f => f.id === 'email')[0].validate, undefined) + assert.notStrictEqual(signInFeature.formFields.filter(f => f.id === 'password')[0].validate, undefined) + + const resetPasswordUsingTokenFeature = emailpassword.config.resetPasswordUsingTokenFeature + + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm.length, 1) + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm[0].id, 'email') + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm.length, 1) + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm[0].id, 'password') + }) + + it('test config for thirdpartyemailpassword module, with provider', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + providers: [customProvider], + }), + ], + }) + + const thirdpartyemailpassword = await ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + const thirdParty = thirdpartyemailpassword.thirdPartyRecipe + + assert.notStrictEqual(thirdParty, undefined) + const emailVerificationFeatureTP = thirdpartyemailpassword.thirdPartyRecipe.config.emailVerificationFeature + + const emailpassword = thirdpartyemailpassword.emailPasswordRecipe + + const signUpFeature = emailpassword.config.signUpFeature + assert.strictEqual(signUpFeature.formFields.length, 2) + assert.strictEqual(signUpFeature.formFields.filter(f => f.id === 'email')[0].optional, false) + assert.strictEqual(signUpFeature.formFields.filter(f => f.id === 'password')[0].optional, false) + assert.notStrictEqual(signUpFeature.formFields.filter(f => f.id === 'email')[0].validate, undefined) + assert.notStrictEqual(signUpFeature.formFields.filter(f => f.id === 'password')[0].validate, undefined) + + const signInFeature = emailpassword.config.signInFeature + assert.strictEqual(signInFeature.formFields.length, 2) + assert.strictEqual(signInFeature.formFields.filter(f => f.id === 'email')[0].optional, false) + assert.strictEqual(signInFeature.formFields.filter(f => f.id === 'password')[0].optional, false) + assert.notStrictEqual(signInFeature.formFields.filter(f => f.id === 'email')[0].validate, undefined) + assert.notStrictEqual(signInFeature.formFields.filter(f => f.id === 'password')[0].validate, undefined) + + const resetPasswordUsingTokenFeature = emailpassword.config.resetPasswordUsingTokenFeature + + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm.length, 1) + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForGenerateTokenForm[0].id, 'email') + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm.length, 1) + assert.strictEqual(resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm[0].id, 'password') + }) +}) diff --git a/test/thirdpartyemailpassword/emailDelivery.test.js b/test/thirdpartyemailpassword/emailDelivery.test.js deleted file mode 100644 index 4c7e31815..000000000 --- a/test/thirdpartyemailpassword/emailDelivery.test.js +++ /dev/null @@ -1,890 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse, delay } = require("../utils"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -const EmailVerification = require("../../recipe/emailverification"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -let { SMTPService } = require("../../recipe/thirdpartyemailpassword/emaildelivery"); -let nock = require("nock"); -let supertest = require("supertest"); -const { middleware, errorHandler } = require("../../framework/express"); -let express = require("express"); - -describe(`emailDelivery: ${printPath("[test/thirdpartyemailpassword/emailDelivery.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await cleanST(); - }); - - it("test default backward compatibility api being called: reset password", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - - let appName = undefined; - let email = undefined; - let passwordResetURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/password/reset") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - passwordResetURL = body.passwordResetURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - await delay(2); - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - }); - - it("test default backward compatibility api being called, error message not sent back to user: reset password", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - - let appName = undefined; - let email = undefined; - let passwordResetURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/password/reset") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - passwordResetURL = body.passwordResetURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - await delay(2); - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - assert.strictEqual(result.body.status, "OK"); - }); - - it("test backward compatibility: reset password (emailpassword user)", async function () { - await startST(); - let email = undefined; - let passwordResetURL = undefined; - let timeJoined = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: async (input, passwordResetLink) => { - email = input.email; - passwordResetURL = passwordResetLink; - timeJoined = input.timeJoined; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - assert.notStrictEqual(timeJoined, undefined); - }); - - it("test backward compatibility: reset password (non-existent user)", async function () { - await startST(); - let functionCalled = false; - let email = undefined; - let passwordResetURL = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: async (input, passwordResetLink) => { - functionCalled = true; - email = input.email; - passwordResetURL = passwordResetLink; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(functionCalled, false); - assert.strictEqual(email, undefined); - assert.strictEqual(passwordResetURL, undefined); - - await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(passwordResetURL, undefined); - }); - - it("test backward compatibility: reset password (thirdparty user)", async function () { - await startST(); - let functionCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: async (input, passwordResetLink) => { - functionCalled = true; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await ThirdPartyEmailPassword.thirdPartySignInUp("custom-provider", "test-user-id", "test@example.com"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(functionCalled, false); - }); - - it("test custom override: reset password", async function () { - await startST(); - let email = undefined; - let passwordResetURL = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - email = input.user.email; - passwordResetURL = input.passwordResetLink; - type = input.type; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/password/reset") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORD_RESET"); - assert.notStrictEqual(passwordResetURL, undefined); - }); - - it("test smtp service: reset password", async function () { - await startST(); - let email = undefined; - let passwordResetURL = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - sendRawEmailCalled = true; - assert.strictEqual(input.body, passwordResetURL); - assert.strictEqual(input.subject, "custom subject"); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORD_RESET"); - passwordResetURL = input.passwordResetLink; - return { - body: input.passwordResetLink, - toEmail: input.user.email, - subject: "custom subject", - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(middleware()); - app.use(errorHandler()); - - await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - - await supertest(app) - .post("/auth/user/password/reset/token") - .set("rid", "thirdpartyemailpassword") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - ], - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.notStrictEqual(passwordResetURL, undefined); - }); - - it("test default backward compatibility api being called: email verify", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - let appName = undefined; - let email = undefined; - let emailVerifyURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - emailVerifyURL = body.emailVerifyURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test default backward compatibility api being called, error message not sent back to user: email verify", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - let appName = undefined; - let email = undefined; - let emailVerifyURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - emailVerifyURL = body.emailVerifyURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - assert.strictEqual(result.body.status, "OK"); - }); - - it("test backward compatibility: email verify (emailpassword user)", async function () { - await startST(); - let idInCallback = undefined; - let email = undefined; - let emailVerifyURL = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { - email = input.email; - idInCallback = input.id; - emailVerifyURL = emailVerificationURLWithToken; - }, - }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - await delay(2); - assert.strictEqual(idInCallback, user.user.id); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test backward compatibility: email verify (thirdparty user)", async function () { - await startST(); - let idInCallback = undefined; - let email = undefined; - let emailVerifyURL = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { - email = input.email; - idInCallback = input.id; - emailVerifyURL = emailVerificationURLWithToken; - }, - }), - ThirdPartyEmailPassword.init({ - // We need to add something to the providers array to make the thirdparty recipe initialize - providers: [/** @type {any} */ {}], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdPartyEmailPassword.thirdPartySignInUp( - "custom-provider", - "test-user-id", - "test@example.com" - ); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - await delay(2); - assert.strictEqual(idInCallback, user.user.id); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test custom override: email verify", async function () { - await startST(); - let email = undefined; - let emailVerifyURL = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - email = input.user.email; - emailVerifyURL = input.emailVerifyLink; - type = input.type; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "EMAIL_VERIFICATION"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test smtp service: email verify", async function () { - await startST(); - let email = undefined; - let emailVerifyURL = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - sendRawEmailCalled = true; - assert.strictEqual(input.body, emailVerifyURL); - assert.strictEqual(input.subject, "custom subject"); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "EMAIL_VERIFICATION"); - emailVerifyURL = input.emailVerifyLink; - return { - body: input.emailVerifyLink, - toEmail: input.user.email, - subject: "custom subject", - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdPartyEmailPassword.emailPasswordSignUp("test@example.com", "1234abcd"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.notStrictEqual(emailVerifyURL, undefined); - }); -}); diff --git a/test/thirdpartyemailpassword/emailDelivery.test.ts b/test/thirdpartyemailpassword/emailDelivery.test.ts new file mode 100644 index 000000000..5b025dbef --- /dev/null +++ b/test/thirdpartyemailpassword/emailDelivery.test.ts @@ -0,0 +1,892 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import ThirdPartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword' +import { SMTPService } from 'supertokens-node/recipe/thirdpartyemailpassword/emaildelivery' +import nock from 'nock' +import supertest from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import express from 'express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, delay, extractInfoFromResponse, killAllST, printPath, setupST, startST } from '../utils' + +describe(`emailDelivery: ${printPath('[test/thirdpartyemailpassword/emailDelivery.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await cleanST() + }) + + it('test default backward compatibility api being called: reset password', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + + let appName + let email + let passwordResetURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/password/reset') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + passwordResetURL = body.passwordResetURL + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + await delay(2) + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + }) + + it('test default backward compatibility api being called, error message not sent back to user: reset password', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + + let appName + let email + let passwordResetURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/password/reset') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + passwordResetURL = body.passwordResetURL + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + await delay(2) + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + assert.strictEqual(result.body.status, 'OK') + }) + + it('test backward compatibility: reset password (emailpassword user)', async () => { + await startST() + let email + let passwordResetURL + let timeJoined + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: async (input, passwordResetLink) => { + email = input.email + passwordResetURL = passwordResetLink + timeJoined = input.timeJoined + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + assert.notStrictEqual(timeJoined, undefined) + }) + + it('test backward compatibility: reset password (non-existent user)', async () => { + await startST() + let functionCalled = false + let email + let passwordResetURL + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: async (input, passwordResetLink) => { + functionCalled = true + email = input.email + passwordResetURL = passwordResetLink + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(functionCalled, false) + assert.strictEqual(email, undefined) + assert.strictEqual(passwordResetURL, undefined) + + await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(passwordResetURL, undefined) + }) + + it('test backward compatibility: reset password (thirdparty user)', async () => { + await startST() + let functionCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + resetPasswordUsingTokenFeature: { + createAndSendCustomEmail: async (input, passwordResetLink) => { + functionCalled = true + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await ThirdPartyEmailPassword.thirdPartySignInUp('custom-provider', 'test-user-id', 'test@example.com') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(functionCalled, false) + }) + + it('test custom override: reset password', async () => { + await startST() + let email + let passwordResetURL + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + email = input.user.email + passwordResetURL = input.passwordResetLink + type = input.type + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/password/reset') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORD_RESET') + assert.notStrictEqual(passwordResetURL, undefined) + }) + + it('test smtp service: reset password', async () => { + await startST() + let email + let passwordResetURL + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + sendRawEmailCalled = true + assert.strictEqual(input.body, passwordResetURL) + assert.strictEqual(input.subject, 'custom subject') + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORD_RESET') + passwordResetURL = input.passwordResetLink + return { + body: input.passwordResetLink, + toEmail: input.user.email, + subject: 'custom subject', + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(middleware()) + app.use(errorHandler()) + + await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + + await supertest(app) + .post('/auth/user/password/reset/token') + .set('rid', 'thirdpartyemailpassword') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + ], + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.notStrictEqual(passwordResetURL, undefined) + }) + + it('test default backward compatibility api being called: email verify', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + let appName + let email + let emailVerifyURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + emailVerifyURL = body.emailVerifyURL + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test default backward compatibility api being called, error message not sent back to user: email verify', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + let appName + let email + let emailVerifyURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + emailVerifyURL = body.emailVerifyURL + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + assert.strictEqual(result.body.status, 'OK') + }) + + it('test backward compatibility: email verify (emailpassword user)', async () => { + await startST() + let idInCallback + let email + let emailVerifyURL + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { + email = input.email + idInCallback = input.id + emailVerifyURL = emailVerificationURLWithToken + }, + }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + await delay(2) + assert.strictEqual(idInCallback, user.user.id) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test backward compatibility: email verify (thirdparty user)', async () => { + await startST() + let idInCallback + let email + let emailVerifyURL + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { + email = input.email + idInCallback = input.id + emailVerifyURL = emailVerificationURLWithToken + }, + }), + ThirdPartyEmailPassword.init({ + // We need to add something to the providers array to make the thirdparty recipe initialize + providers: [{}], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdPartyEmailPassword.thirdPartySignInUp( + 'custom-provider', + 'test-user-id', + 'test@example.com', + ) + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + await delay(2) + assert.strictEqual(idInCallback, user.user.id) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test custom override: email verify', async () => { + await startST() + let email + let emailVerifyURL + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + email = input.user.email + emailVerifyURL = input.emailVerifyLink + type = input.type + await oI.sendEmail(input) + }, + } + }, + }, + }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'EMAIL_VERIFICATION') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test smtp service: email verify', async () => { + await startST() + let email + let emailVerifyURL + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + sendRawEmailCalled = true + assert.strictEqual(input.body, emailVerifyURL) + assert.strictEqual(input.subject, 'custom subject') + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'EMAIL_VERIFICATION') + emailVerifyURL = input.emailVerifyLink + return { + body: input.emailVerifyLink, + toEmail: input.user.email, + subject: 'custom subject', + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdPartyEmailPassword.emailPasswordSignUp('test@example.com', '1234abcd') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.notStrictEqual(emailVerifyURL, undefined) + }) +}) diff --git a/test/thirdpartyemailpassword/emailExists.test.js b/test/thirdpartyemailpassword/emailExists.test.js deleted file mode 100644 index 780cb25bc..000000000 --- a/test/thirdpartyemailpassword/emailExists.test.js +++ /dev/null @@ -1,214 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, signUPRequest } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -const { Querier } = require("../../lib/build/querier"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -const request = require("supertest"); -const express = require("express"); -let bodyParser = require("body-parser"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`emailExists: ${printPath("[test/thirdpartyemailpassword/emailExists.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // disable the email exists API, and check that calling it returns a 404. - it("test that if disable api, the default email exists API does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailPasswordEmailExistsGET: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(response.status === 404); - }); - - // email exists - it("test good input, email exists", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validPass123"); - assert(signUpResponse.status === 200); - assert(JSON.parse(signUpResponse.text).status === "OK"); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === true); - }); - - //email does not exist - it("test good input, email does not exists", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "random@gmail.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(Object.keys(response).length === 2); - assert(response.status === "OK"); - assert(response.exists === false); - }); - - // testing error is correctly handled by the sub-recipe - it("test bad input, do not pass email", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query() - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide the email as a GET param"); - }); -}); diff --git a/test/thirdpartyemailpassword/emailExists.test.ts b/test/thirdpartyemailpassword/emailExists.test.ts new file mode 100644 index 000000000..5a68bf473 --- /dev/null +++ b/test/thirdpartyemailpassword/emailExists.test.ts @@ -0,0 +1,214 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import assert from 'assert' +import STExpress from 'supertokens-node' + +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword' +import request from 'supertest' +import express from 'express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' + +describe(`emailExists: ${printPath('[test/thirdpartyemailpassword/emailExists.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // disable the email exists API, and check that calling it returns a 404. + it('test that if disable api, the default email exists API does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + emailPasswordEmailExistsGET: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(response.status === 404) + }) + + // email exists + it('test good input, email exists', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validPass123') + assert(signUpResponse.status === 200) + assert(JSON.parse(signUpResponse.text).status === 'OK') + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === true) + }) + + // email does not exist + it('test good input, email does not exists', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'random@gmail.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(Object.keys(response).length === 2) + assert(response.status === 'OK') + assert(response.exists === false) + }) + + // testing error is correctly handled by the sub-recipe + it('test bad input, do not pass email', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query() + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide the email as a GET param') + }) +}) diff --git a/test/thirdpartyemailpassword/emailverify.test.js b/test/thirdpartyemailpassword/emailverify.test.js deleted file mode 100644 index 3da225599..000000000 --- a/test/thirdpartyemailpassword/emailverify.test.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - signUPRequest, - signInUPCustomRequest, - extractInfoFromResponse, - emailVerifyTokenRequest, -} = require("../utils"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -const EmailVerification = require("../../recipe/emailverification"); -const express = require("express"); -const request = require("supertest"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`emailverify: ${printPath("[test/thirdpartyemailpassword/emailverify.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: authCodeResponse.id, - email: { - id: authCodeResponse.email, - isVerified: false, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test the generate token api with valid input, email not verified", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - - assert(JSON.parse(response.text).status === "OK"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - it("test the generate token api with valid input, email verified and test error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - let verifyToken = await EmailVerification.createEmailVerificationToken(userId); - await EmailVerification.verifyEmailUsingToken(verifyToken.token); - - response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId); - - assert(JSON.parse(response.text).status === "EMAIL_ALREADY_VERIFIED_ERROR"); - assert(response.status === 200); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - it("test the generate token api with valid input, no session and check output", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify/token") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(response.status === 401); - assert(JSON.parse(response.text).message === "unauthorised"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); - - it("test that providing your own email callback and make sure it is called", async function () { - await startST(); - - let userInfo = null; - let emailToken = null; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - userInfo = user; - emailToken = emailVerificationURLWithToken; - }, - }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "test@gmail.com", "testPass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - let response2 = await emailVerifyTokenRequest( - app, - infoFromResponse.accessToken, - infoFromResponse.antiCsrf, - userId - ); - - assert(response2.status === 200); - - assert(JSON.parse(response2.text).status === "OK"); - assert(Object.keys(JSON.parse(response2.text)).length === 1); - - assert(userInfo.id === userId); - assert(userInfo.email === "test@gmail.com"); - assert(emailToken !== null); - }); - - it("test that providing your own email callback and make sure it is called (thirdparty user)", async function () { - await startST(); - - let userInfo = null; - let emailToken = null; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { - userInfo = user; - emailToken = emailVerificationURLWithToken; - }, - }), - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signInUPCustomRequest(app, "test@gmail.com", "testPass0"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userId = JSON.parse(response.text).user.id; - let infoFromResponse = extractInfoFromResponse(response); - - let response2 = await emailVerifyTokenRequest( - app, - infoFromResponse.accessToken, - infoFromResponse.antiCsrf, - userId - ); - - assert(response2.status === 200); - - assert(JSON.parse(response2.text).status === "OK"); - assert(Object.keys(JSON.parse(response2.text)).length === 1); - - assert(userInfo.id === userId); - assert(userInfo.email === "test@gmail.com"); - assert(emailToken !== null); - }); - - it("test the email verify API with invalid token and check error", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - }), - ThirdPartyEmailPassword.init(), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - response = await new Promise((resolve) => - request(app) - .post("/auth/user/email/verify") - .send({ - method: "token", - token: "randomToken", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err); - } else { - resolve(res); - } - }) - ); - assert(JSON.parse(response.text).status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"); - assert(Object.keys(JSON.parse(response.text)).length === 1); - }); -}); diff --git a/test/thirdpartyemailpassword/emailverify.test.ts b/test/thirdpartyemailpassword/emailverify.test.ts new file mode 100644 index 000000000..29d42c93c --- /dev/null +++ b/test/thirdpartyemailpassword/emailverify.test.ts @@ -0,0 +1,362 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import ProcessState from 'supertokens-node/processState' +import ThirdPartyEmailPassword, { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + emailVerifyTokenRequest, + extractInfoFromResponse, + killAllST, + printPath, + setupST, + signInUPCustomRequest, + signUPRequest, + startST, +} from '../utils' + +describe(`emailverify: ${printPath('[test/thirdpartyemailpassword/emailverify.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: authCodeResponse.id, + email: { + id: authCodeResponse.email, + isVerified: false, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test the generate token api with valid input, email not verified', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + + assert(JSON.parse(response.text).status === 'OK') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + it('test the generate token api with valid input, email verified and test error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + const verifyToken = await EmailVerification.createEmailVerificationToken(userId) + await EmailVerification.verifyEmailUsingToken(verifyToken.token) + + response = await emailVerifyTokenRequest(app, infoFromResponse.accessToken, infoFromResponse.antiCsrf, userId) + + assert(JSON.parse(response.text).status === 'EMAIL_ALREADY_VERIFIED_ERROR') + assert(response.status === 200) + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + it('test the generate token api with valid input, no session and check output', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify/token') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(response.status === 401) + assert(JSON.parse(response.text).message === 'unauthorised') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) + + it('test that providing your own email callback and make sure it is called', async () => { + await startST() + + let userInfo = null + let emailToken = null + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + userInfo = user + emailToken = emailVerificationURLWithToken + }, + }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'test@gmail.com', 'testPass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + const response2 = await emailVerifyTokenRequest( + app, + infoFromResponse.accessToken, + infoFromResponse.antiCsrf, + userId, + ) + + assert(response2.status === 200) + + assert(JSON.parse(response2.text).status === 'OK') + assert(Object.keys(JSON.parse(response2.text)).length === 1) + + assert(userInfo.id === userId) + assert(userInfo.email === 'test@gmail.com') + assert(emailToken !== null) + }) + + it('test that providing your own email callback and make sure it is called (thirdparty user)', async () => { + await startST() + + let userInfo = null + let emailToken = null + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: (user, emailVerificationURLWithToken) => { + userInfo = user + emailToken = emailVerificationURLWithToken + }, + }), + ThirdPartyEmailPassword.init({ + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signInUPCustomRequest(app, 'test@gmail.com', 'testPass0') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userId = JSON.parse(response.text).user.id + const infoFromResponse = extractInfoFromResponse(response) + + const response2 = await emailVerifyTokenRequest( + app, + infoFromResponse.accessToken, + infoFromResponse.antiCsrf, + userId, + ) + + assert(response2.status === 200) + + assert(JSON.parse(response2.text).status === 'OK') + assert(Object.keys(JSON.parse(response2.text)).length === 1) + + assert(userInfo.id === userId) + assert(userInfo.email === 'test@gmail.com') + assert(emailToken !== null) + }) + + it('test the email verify API with invalid token and check error', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + }), + ThirdPartyEmailPassword.init(), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/user/email/verify') + .send({ + method: 'token', + token: 'randomToken', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err) + + else + resolve(res) + }), + ) + assert(JSON.parse(response.text).status === 'EMAIL_VERIFICATION_INVALID_TOKEN_ERROR') + assert(Object.keys(JSON.parse(response.text)).length === 1) + }) +}) diff --git a/test/thirdpartyemailpassword/getUsersByEmailFeature.test.js b/test/thirdpartyemailpassword/getUsersByEmailFeature.test.js deleted file mode 100644 index 7fd8315e3..000000000 --- a/test/thirdpartyemailpassword/getUsersByEmailFeature.test.js +++ /dev/null @@ -1,101 +0,0 @@ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -const { - thirdPartySignInUp, - getUsersByEmail, - emailPasswordSignUp, -} = require("../../lib/build/recipe/thirdpartyemailpassword"); -const { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`getUsersByEmail: ${printPath("[test/thirdpartyemailpassword/getUsersByEmailFeature.test.js]")}`, function () { - const MockThirdPartyProvider = { - id: "mock", - }; - - const MockThirdPartyProvider2 = { - id: "mock2", - }; - - const testSTConfig = { - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider], - }, - }), - ], - }; - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("invalid email yields empty users array", async function () { - await startST(); - STExpress.init(testSTConfig); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - // given there are no users - - // when - const thirdPartyUsers = await getUsersByEmail("john.doe@example.com"); - - // then - assert.strictEqual(thirdPartyUsers.length, 0); - }); - - it("valid email yields third party users", async function () { - await startST(); - STExpress.init({ - ...testSTConfig, - recipeList: [ - ThirdPartyEmailPassword.init({ - signInAndUpFeature: { - providers: [MockThirdPartyProvider, MockThirdPartyProvider2], - }, - }), - ], - }); - - let apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(apiVersion, "2.7") === "2.7") { - return; - } - - await emailPasswordSignUp("john.doe@example.com", "somePass"); - await thirdPartySignInUp("mock", "thirdPartyJohnDoe", "john.doe@example.com"); - await thirdPartySignInUp("mock2", "thirdPartyDaveDoe", "john.doe@example.com"); - - const thirdPartyUsers = await getUsersByEmail("john.doe@example.com"); - - assert.strictEqual(thirdPartyUsers.length, 3); - - thirdPartyUsers.forEach((user) => { - assert.strictEqual(user.email, "john.doe@example.com"); - }); - }); -}); diff --git a/test/thirdpartyemailpassword/getUsersByEmailFeature.test.ts b/test/thirdpartyemailpassword/getUsersByEmailFeature.test.ts new file mode 100644 index 000000000..640057183 --- /dev/null +++ b/test/thirdpartyemailpassword/getUsersByEmailFeature.test.ts @@ -0,0 +1,94 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyEmailPassword, { emailPasswordSignUp, getUsersByEmail, thirdPartySignInUp } from 'supertokens-node/recipe/thirdpartyemailpassword' +import { maxVersion } from 'supertokens-node/utils' +import { Querier } from 'supertokens-node/querier' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getUsersByEmail: ${printPath('[test/thirdpartyemailpassword/getUsersByEmailFeature.test.ts]')}`, () => { + const MockThirdPartyProvider = { + id: 'mock', + } + + const MockThirdPartyProvider2 = { + id: 'mock2', + } + + const testSTConfig = { + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + signInAndUpFeature: { + providers: [MockThirdPartyProvider], + }, + }), + ], + } + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('invalid email yields empty users array', async () => { + await startST() + STExpress.init(testSTConfig) + + const apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + // given there are no users + + // when + const thirdPartyUsers = await getUsersByEmail('john.doe@example.com') + + // then + assert.strictEqual(thirdPartyUsers.length, 0) + }) + + it('valid email yields third party users', async () => { + await startST() + STExpress.init({ + ...testSTConfig, + recipeList: [ + ThirdPartyEmailPassword.init({ + signInAndUpFeature: { + providers: [MockThirdPartyProvider, MockThirdPartyProvider2], + }, + }), + ], + }) + + const apiVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(apiVersion, '2.7') === '2.7') + return + + await emailPasswordSignUp('john.doe@example.com', 'somePass') + await thirdPartySignInUp('mock', 'thirdPartyJohnDoe', 'john.doe@example.com') + await thirdPartySignInUp('mock2', 'thirdPartyDaveDoe', 'john.doe@example.com') + + const thirdPartyUsers = await getUsersByEmail('john.doe@example.com') + + assert.strictEqual(thirdPartyUsers.length, 3) + + thirdPartyUsers.forEach((user) => { + assert.strictEqual(user.email, 'john.doe@example.com') + }) + }) +}) diff --git a/test/thirdpartyemailpassword/override.test.js b/test/thirdpartyemailpassword/override.test.js deleted file mode 100644 index 9d83581b4..000000000 --- a/test/thirdpartyemailpassword/override.test.js +++ /dev/null @@ -1,563 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, stopST, killAllST, cleanST, resetAll, signUPRequest } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -const { Querier } = require("../../lib/build/querier"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -const express = require("express"); -const request = require("supertest"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`overrideTest: ${printPath("[test/thirdpartyemailpassword/override.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("overriding functions tests", async () => { - await startST(); - let user = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - functions: (oI) => { - return { - ...oI, - emailPasswordSignUp: async (input) => { - let response = await oI.emailPasswordSignUp(input); - if (response.status === "OK") { - user = response.user; - } - return response; - }, - emailPasswordSignIn: async (input) => { - let response = await oI.emailPasswordSignIn(input); - if (response.status === "OK") { - user = response.user; - } - return response; - }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); - }); - - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signInResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: signInResponse.user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); - }); - - it("overriding api tests", async () => { - await startST(); - let user = undefined; - let newUser = undefined; - let emailExists = undefined; - let type = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailPasswordSignInPOST: async (input) => { - let response = await oI.emailPasswordSignInPOST(input); - if (response.status === "OK") { - user = response.user; - newUser = false; - type = "emailpassword"; - } - return response; - }, - emailPasswordSignUpPOST: async (input) => { - let response = await oI.emailPasswordSignUpPOST(input); - if (response.status === "OK") { - user = response.user; - newUser = true; - type = "emailpassword"; - } - return response; - }, - emailPasswordEmailExistsGET: async (input) => { - let response = await oI.emailPasswordEmailExistsGET(input); - emailExists = response.exists; - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); - }); - - let emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.strictEqual(emailExistsResponse.exists, false); - assert.strictEqual(emailExists, false); - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.strictEqual(type, "emailpassword"); - assert.deepStrictEqual(signUpResponse.body.user, user); - - emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.strictEqual(emailExistsResponse.exists, true); - assert.strictEqual(emailExists, true); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, false); - assert.strictEqual(type, "emailpassword"); - assert.deepStrictEqual(signInResponse.user, user); - }); - - it("overriding functions tests, throws error", async () => { - await startST(); - let user = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - functions: (oI) => { - return { - ...oI, - emailPasswordSignUp: async (input) => { - let response = await oI.emailPasswordSignUp(input); - user = response.user; - throw { - error: "signup error", - }; - }, - emailPasswordSignIn: async (input) => { - await oI.emailPasswordSignIn(input); - throw { - error: "signin error", - }; - }, - getUserById: async (input) => { - await oI.getUserById(input); - throw { - error: "get user error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res, next) => { - try { - let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); - } catch (err) { - next(err); - } - }); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse.body, { error: "signup error", customError: true }); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(userByIdResponse, { error: "get user error", customError: true }); - }); - - it("overriding api tests, throws error", async () => { - await startST(); - let user = undefined; - let newUser = undefined; - let emailExists = undefined; - let type = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailPasswordSignInPOST: async (input) => { - let response = await oI.emailPasswordSignInPOST(input); - user = response.user; - newUser = false; - type = "emailpassword"; - if (newUser) { - throw { - error: "signup error", - }; - } - throw { - error: "signin error", - }; - }, - emailPasswordSignUpPOST: async (input) => { - let response = await oI.emailPasswordSignUpPOST(input); - user = response.user; - newUser = true; - type = "emailpassword"; - if (newUser) { - throw { - error: "signup error", - }; - } - throw { - error: "signin error", - }; - }, - emailPasswordEmailExistsGET: async (input) => { - let response = await oI.emailPasswordEmailExistsGET(input); - emailExists = response.exists; - throw { - error: "email exists error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdPartyEmailPassword.getUserById(userId)); - }); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - assert.deepStrictEqual(emailExistsResponse, { error: "email exists error", customError: true }); - assert.strictEqual(emailExists, false); - let signUpResponse = await signUPRequest(app, "user@test.com", "test123!"); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.strictEqual(type, "emailpassword"); - assert.deepStrictEqual(signUpResponse.body, { error: "signup error", customError: true }); - - emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "user@test.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - assert.deepStrictEqual(emailExistsResponse, { error: "email exists error", customError: true }); - assert.strictEqual(emailExists, true); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "test123!", - }, - { - id: "email", - value: "user@test.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - }); -}); diff --git a/test/thirdpartyemailpassword/override.test.ts b/test/thirdpartyemailpassword/override.test.ts new file mode 100644 index 000000000..f4c7b55e2 --- /dev/null +++ b/test/thirdpartyemailpassword/override.test.ts @@ -0,0 +1,564 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import assert from 'assert' +import STExpress from 'supertokens-node' + +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyEmailPassword from 'supertokens-node/recipe/thirdpartyemailpassword' +import express from 'express' +import request from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' + +describe(`overrideTest: ${printPath('[test/thirdpartyemailpassword/override.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('overriding functions tests', async () => { + await startST() + let user + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + functions: (oI) => { + return { + ...oI, + emailPasswordSignUp: async (input) => { + const response = await oI.emailPasswordSignUp(input) + if (response.status === 'OK') + user = response.user + + return response + }, + emailPasswordSignIn: async (input) => { + const response = await oI.emailPasswordSignIn(input) + if (response.status === 'OK') + user = response.user + + return response + }, + getUserById: async (input) => { + const response = await oI.getUserById(input) + user = response + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await ThirdPartyEmailPassword.getUserById(userId)) + }) + + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse.body.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signInResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: signInResponse.user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(userByIdResponse, user) + }) + + it('overriding api tests', async () => { + await startST() + let user + let newUser + let emailExists + let type + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + emailPasswordSignInPOST: async (input) => { + const response = await oI.emailPasswordSignInPOST(input) + if (response.status === 'OK') { + user = response.user + newUser = false + type = 'emailpassword' + } + return response + }, + emailPasswordSignUpPOST: async (input) => { + const response = await oI.emailPasswordSignUpPOST(input) + if (response.status === 'OK') { + user = response.user + newUser = true + type = 'emailpassword' + } + return response + }, + emailPasswordEmailExistsGET: async (input) => { + const response = await oI.emailPasswordEmailExistsGET(input) + emailExists = response.exists + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await ThirdPartyEmailPassword.getUserById(userId)) + }) + + let emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.strictEqual(emailExistsResponse.exists, false) + assert.strictEqual(emailExists, false) + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.strictEqual(type, 'emailpassword') + assert.deepStrictEqual(signUpResponse.body.user, user) + + emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.strictEqual(emailExistsResponse.exists, true) + assert.strictEqual(emailExists, true) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, false) + assert.strictEqual(type, 'emailpassword') + assert.deepStrictEqual(signInResponse.user, user) + }) + + it('overriding functions tests, throws error', async () => { + await startST() + let user + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + functions: (oI) => { + return { + ...oI, + emailPasswordSignUp: async (input) => { + const response = await oI.emailPasswordSignUp(input) + user = response.user + throw { + error: 'signup error', + } + }, + emailPasswordSignIn: async (input) => { + await oI.emailPasswordSignIn(input) + throw { + error: 'signin error', + } + }, + getUserById: async (input) => { + await oI.getUserById(input) + throw { + error: 'get user error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res, next) => { + try { + const userId = req.query.userId + res.json(await ThirdPartyEmailPassword.getUserById(userId)) + } + catch (err) { + next(err) + } + }) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse.body, { error: 'signup error', customError: true }) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(userByIdResponse, { error: 'get user error', customError: true }) + }) + + it('overriding api tests, throws error', async () => { + await startST() + let user + let newUser + let emailExists + let type + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + emailPasswordSignInPOST: async (input) => { + const response = await oI.emailPasswordSignInPOST(input) + user = response.user + newUser = false + type = 'emailpassword' + if (newUser) { + throw { + error: 'signup error', + } + } + throw { + error: 'signin error', + } + }, + emailPasswordSignUpPOST: async (input) => { + const response = await oI.emailPasswordSignUpPOST(input) + user = response.user + newUser = true + type = 'emailpassword' + if (newUser) { + throw { + error: 'signup error', + } + } + throw { + error: 'signin error', + } + }, + emailPasswordEmailExistsGET: async (input) => { + const response = await oI.emailPasswordEmailExistsGET(input) + emailExists = response.exists + throw { + error: 'email exists error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await ThirdPartyEmailPassword.getUserById(userId)) + }) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + let emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + assert.deepStrictEqual(emailExistsResponse, { error: 'email exists error', customError: true }) + assert.strictEqual(emailExists, false) + const signUpResponse = await signUPRequest(app, 'user@test.com', 'test123!') + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.strictEqual(type, 'emailpassword') + assert.deepStrictEqual(signUpResponse.body, { error: 'signup error', customError: true }) + + emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'user@test.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + assert.deepStrictEqual(emailExistsResponse, { error: 'email exists error', customError: true }) + assert.strictEqual(emailExists, true) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'test123!', + }, + { + id: 'email', + value: 'user@test.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.strictEqual(newUser, false) + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + }) +}) diff --git a/test/thirdpartyemailpassword/signinFeature.test.js b/test/thirdpartyemailpassword/signinFeature.test.js deleted file mode 100644 index 7e1bf1be3..000000000 --- a/test/thirdpartyemailpassword/signinFeature.test.js +++ /dev/null @@ -1,960 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - signUPRequestEmptyJSON, - signUPRequest, - signUPRequestNoBody, -} = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPassword = require("../../recipe/thirdpartyemailpassword"); -const express = require("express"); -const request = require("supertest"); -let nock = require("nock"); -const { response } = require("express"); -let { middleware, errorHandler } = require("../../framework/express"); -let bodyParser = require("body-parser"); - -describe(`signinFeature: ${printPath("[test/thirdpartyemailpassword/signinFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test that disable api, the default signinup API does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: undefined, - }; - }, - }, - providers: [ - ThirdPartyEmailPassword.Google({ - clientId: "test", - clientSecret: "test-secret", - }), - ], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "google", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.status, 404); - }); - - it("test that disable api, the default signin API does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailPasswordSignInPOST: undefined, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 404); - }); - - it("test handlePostSignUpIn gets set correctly", async function () { - await startST(); - - process.env.userId = ""; - process.env.loginType = ""; - - assert.strictEqual(process.env.userId, ""); - assert.strictEqual(process.env.loginType, ""); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: async (input) => { - let response = await oI.thirdPartySignInUpPOST(input); - if (response.status === "OK") { - process.env.userId = response.user.id; - process.env.loginType = "thirdparty"; - } - return response; - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.strictEqual(process.env.userId, response1.body.user.id); - assert.strictEqual(process.env.loginType, "thirdparty"); - }); - - it("test singinAPI works when input is fine", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - it("test singinAPI works when input is fine and user has added JSON middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.json()); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - it("test singinAPI works when input is fine and user has added urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - it("test singinAPI works when input is fine and user has added both JSON and urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.json()); - app.use(express.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - it("test singinAPI works when input is fine and user has added bodyParser JSON middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(bodyParser.json()); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - it("test singinAPI works when input is fine and user has added bodyParser urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(bodyParser.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - it("test singinAPI works when input is fine and user has added both bodyParser JSON and bodyParser urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let signUpUserInfo = JSON.parse(response.text).user; - - let userInfo = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text).user); - } - }) - ); - assert(userInfo.id === signUpUserInfo.id); - assert(userInfo.email === signUpUserInfo.email); - }); - - it("test singinAPI with empty JSON and user has added JSON middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.json()); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequestEmptyJSON(app); - assert(JSON.parse(response.text).message === "Missing input param: formFields"); - assert(response.status === 400); - }); - - it("test singinAPI with empty JSON and user has added urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequestEmptyJSON(app); - assert(JSON.parse(response.text).message === "Missing input param: formFields"); - assert(response.status === 400); - }); - - it("test singinAPI with empty JSON and user has added both JSON and urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.json()); - app.use(express.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequestEmptyJSON(app); - assert(JSON.parse(response.text).message === "Missing input param: formFields"); - assert(response.status === 400); - }); - - it("test singinAPI with empty JSON and user has added both bodyParser JSON and bodyParser urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequestEmptyJSON(app); - assert(JSON.parse(response.text).message === "Missing input param: formFields"); - assert(response.status === 400); - }); - - it("test singinAPI with empty request body and user has added JSON middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.json()); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequestNoBody(app); - assert(JSON.parse(response.text).message === "Missing input param: formFields"); - assert(response.status === 400); - }); - - it("test singinAPI with empty request body and user has added urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequestNoBody(app); - assert(JSON.parse(response.text).message === "Missing input param: formFields"); - assert(response.status === 400); - }); - - it("test singinAPI with empty request body and user has added both JSON and urlencoded middleware", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(express.json()); - app.use(express.urlencoded({ extended: true })); - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequestNoBody(app); - assert(JSON.parse(response.text).message === "Missing input param: formFields"); - assert(response.status === 400); - }); - - /* - Setting the email value in form field as random@gmail.com causes the test to fail - */ - // testing error gets corectly routed to sub-recipe - it("test singinAPI throws an error when email does not match", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let invalidEmailResponse = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "ran@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(invalidEmailResponse.status === "WRONG_CREDENTIALS_ERROR"); - }); - - /* - having the email start with "test" (requierment of the custom validator) will cause the test to fail - */ - it("test custom email validators to sign up and make sure they are applied to sign in", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - signUpFeature: { - formFields: [ - { - id: "email", - validate: (value) => { - if (value.startsWith("test")) { - return undefined; - } - return "email does not start with test"; - }, - }, - ], - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let signUpResponse = await signUPRequest(app, "testrandom@gmail.com", "validpass123"); - - assert(JSON.parse(signUpResponse.text).status === "OK"); - assert(signUpResponse.status === 200); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - } - resolve(JSON.parse(res.text)); - }) - ); - assert(response.status === "FIELD_ERROR"); - assert(response.formFields[0].error === "email does not start with test"); - assert(response.formFields[0].id === "email"); - }); -}); diff --git a/test/thirdpartyemailpassword/signinFeature.test.ts b/test/thirdpartyemailpassword/signinFeature.test.ts new file mode 100644 index 000000000..91012fece --- /dev/null +++ b/test/thirdpartyemailpassword/signinFeature.test.ts @@ -0,0 +1,961 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyEmailPassword, { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword' +import express from 'express' +import request from 'supertest' +import nock from 'nock' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import bodyParser from 'body-parser' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + killAllST, + printPath, + setupST, + signUPRequest, + signUPRequestEmptyJSON, + signUPRequestNoBody, + startST, +} from '../utils' + +describe(`signinFeature: ${printPath('[test/thirdpartyemailpassword/signinFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test that disable api, the default signinup API does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: undefined, + } + }, + }, + providers: [ + ThirdPartyEmailPassword.Google({ + clientId: 'test', + clientSecret: 'test-secret', + }), + ], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'google', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.status, 404) + }) + + it('test that disable api, the default signin API does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + emailPasswordSignInPOST: undefined, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 404) + }) + + it('test handlePostSignUpIn gets set correctly', async () => { + await startST() + + process.env.userId = '' + process.env.loginType = '' + + assert.strictEqual(process.env.userId, '') + assert.strictEqual(process.env.loginType, '') + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyEmailPassword.init({ + providers: [customProvider1], + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: async (input) => { + const response = await oI.thirdPartySignInUpPOST(input) + if (response.status === 'OK') { + process.env.userId = response.user.id + process.env.loginType = 'thirdparty' + } + return response + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.strictEqual(process.env.userId, response1.body.user.id) + assert.strictEqual(process.env.loginType, 'thirdparty') + }) + + it('test singinAPI works when input is fine', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + it('test singinAPI works when input is fine and user has added JSON middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.json()) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + it('test singinAPI works when input is fine and user has added urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + it('test singinAPI works when input is fine and user has added both JSON and urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.json()) + app.use(express.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + it('test singinAPI works when input is fine and user has added bodyParser JSON middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(bodyParser.json()) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + it('test singinAPI works when input is fine and user has added bodyParser urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(bodyParser.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + it('test singinAPI works when input is fine and user has added both bodyParser JSON and bodyParser urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const signUpUserInfo = JSON.parse(response.text).user + + const userInfo = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text).user) + }), + ) + assert(userInfo.id === signUpUserInfo.id) + assert(userInfo.email === signUpUserInfo.email) + }) + + it('test singinAPI with empty JSON and user has added JSON middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.json()) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequestEmptyJSON(app) + assert(JSON.parse(response.text).message === 'Missing input param: formFields') + assert(response.status === 400) + }) + + it('test singinAPI with empty JSON and user has added urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequestEmptyJSON(app) + assert(JSON.parse(response.text).message === 'Missing input param: formFields') + assert(response.status === 400) + }) + + it('test singinAPI with empty JSON and user has added both JSON and urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.json()) + app.use(express.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequestEmptyJSON(app) + assert(JSON.parse(response.text).message === 'Missing input param: formFields') + assert(response.status === 400) + }) + + it('test singinAPI with empty JSON and user has added both bodyParser JSON and bodyParser urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequestEmptyJSON(app) + assert(JSON.parse(response.text).message === 'Missing input param: formFields') + assert(response.status === 400) + }) + + it('test singinAPI with empty request body and user has added JSON middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.json()) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequestNoBody(app) + assert(JSON.parse(response.text).message === 'Missing input param: formFields') + assert(response.status === 400) + }) + + it('test singinAPI with empty request body and user has added urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequestNoBody(app) + assert(JSON.parse(response.text).message === 'Missing input param: formFields') + assert(response.status === 400) + }) + + it('test singinAPI with empty request body and user has added both JSON and urlencoded middleware', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(express.json()) + app.use(express.urlencoded({ extended: true })) + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequestNoBody(app) + assert(JSON.parse(response.text).message === 'Missing input param: formFields') + assert(response.status === 400) + }) + + /* + Setting the email value in form field as random@gmail.com causes the test to fail + */ + // testing error gets corectly routed to sub-recipe + it('test singinAPI throws an error when email does not match', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const invalidEmailResponse = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'ran@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(invalidEmailResponse.status === 'WRONG_CREDENTIALS_ERROR') + }) + + /* + having the email start with "test" (requierment of the custom validator) will cause the test to fail + */ + it('test custom email validators to sign up and make sure they are applied to sign in', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: 'email', + validate: (value) => { + if (value.startsWith('test')) + return undefined + + return 'email does not start with test' + }, + }, + ], + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const signUpResponse = await signUPRequest(app, 'testrandom@gmail.com', 'validpass123') + + assert(JSON.parse(signUpResponse.text).status === 'OK') + assert(signUpResponse.status === 200) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined) + } + else { + } + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'FIELD_ERROR') + assert(response.formFields[0].error === 'email does not start with test') + assert(response.formFields[0].id === 'email') + }) +}) diff --git a/test/thirdpartyemailpassword/signoutFeature.test.js b/test/thirdpartyemailpassword/signoutFeature.test.js deleted file mode 100644 index 0921aec06..000000000 --- a/test/thirdpartyemailpassword/signoutFeature.test.js +++ /dev/null @@ -1,455 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - signUPRequest, - extractInfoFromResponse, - setKeyValueInConfig, -} = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPasswordRecipe = require("../../lib/build/recipe/thirdpartyemailpassword/recipe").default; -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signoutTest: ${printPath("[test/thirdpartyemailpassword/signoutFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test the default route and it should revoke the session", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPasswordRecipe.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.statusCode, 200); - - let res1 = extractInfoFromResponse(response1); - - let response2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - assert.strictEqual(response2.antiCsrf, undefined); - assert.strictEqual(response2.accessToken, ""); - assert.strictEqual(response2.refreshToken, ""); - assert.strictEqual(response2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.accessTokenDomain, undefined); - assert.strictEqual(response2.refreshTokenDomain, undefined); - assert.strictEqual(response2.frontToken, "remove"); - - let response3 = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response3.text).status === "OK"); - assert(response3.status === 200); - - let res2 = extractInfoFromResponse(response3); - - let response4 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - assert.strictEqual(response4.antiCsrf, undefined); - assert.strictEqual(response4.accessToken, ""); - assert.strictEqual(response4.refreshToken, ""); - assert.strictEqual(response4.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response4.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response4.accessTokenDomain, undefined); - assert.strictEqual(response4.refreshTokenDomain, undefined); - assert.strictEqual(response4.frontToken, "remove"); - }); - - it("test that disabling default route and calling the API returns 404", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPasswordRecipe.init({ - providers: [this.customProvider1], - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "thirdpartyemailpassword") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 404); - }); - - it("test that calling the API without a session should return OK", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPasswordRecipe.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.body.status, "OK"); - assert.strictEqual(response.status, 200); - assert.strictEqual(response.header["set-cookie"], undefined); - }); - - it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPasswordRecipe.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.statusCode, 200); - - let res1 = extractInfoFromResponse(response1); - - await new Promise((r) => setTimeout(r, 5000)); - - let signOutResponse = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + res1.accessToken]) - .set("anti-csrf", res1.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(signOutResponse.status, 401); - assert.strictEqual(signOutResponse.body.message, "try refresh token"); - - let refreshedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .expect(200) - .set("rid", "session") - .set("Cookie", ["sRefreshToken=" + res1.refreshToken]) - .set("anti-csrf", res1.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - signOutResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + refreshedResponse.accessToken]) - .set("anti-csrf", refreshedResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(signOutResponse.antiCsrf, undefined); - assert.strictEqual(signOutResponse.accessToken, ""); - assert.strictEqual(signOutResponse.refreshToken, ""); - assert.strictEqual(signOutResponse.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(signOutResponse.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(signOutResponse.accessTokenDomain, undefined); - assert.strictEqual(signOutResponse.refreshTokenDomain, undefined); - let response2 = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response2.text).status === "OK"); - assert(response2.status === 200); - - let res2 = extractInfoFromResponse(response2); - - await new Promise((r) => setTimeout(r, 5000)); - - signOutResponse = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + res2.accessToken]) - .set("anti-csrf", res2.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(signOutResponse.status === 401); - assert(JSON.parse(signOutResponse.text).message === "try refresh token"); - - refreshedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("rid", "session") - .set("Cookie", ["sRefreshToken=" + res2.refreshToken]) - .set("anti-csrf", res2.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - signOutResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + refreshedResponse.accessToken]) - .set("anti-csrf", refreshedResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert(signOutResponse.antiCsrf === undefined); - assert(signOutResponse.accessToken === ""); - assert(signOutResponse.refreshToken === ""); - assert(signOutResponse.accessTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(signOutResponse.refreshTokenExpiry === "Thu, 01 Jan 1970 00:00:00 GMT"); - assert(signOutResponse.accessTokenDomain === undefined); - assert(signOutResponse.refreshTokenDomain === undefined); - }); -}); diff --git a/test/thirdpartyemailpassword/signoutFeature.test.ts b/test/thirdpartyemailpassword/signoutFeature.test.ts new file mode 100644 index 000000000..fe6f67c90 --- /dev/null +++ b/test/thirdpartyemailpassword/signoutFeature.test.ts @@ -0,0 +1,461 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyEmailPasswordRecipe from 'supertokens-node/recipe/thirdpartyemailpassword/recipe' +import nock from 'nock' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword' +import { + cleanST, + extractInfoFromResponse, + killAllST, + printPath, + setKeyValueInConfig, + setupST, + signUPRequest, + startST, +} from '../utils' + +describe(`signoutTest: ${printPath('[test/thirdpartyemailpassword/signoutFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test the default route and it should revoke the session', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPasswordRecipe.init({ + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.statusCode, 200) + + const res1 = extractInfoFromResponse(response1) + + const response2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + assert.strictEqual(response2.antiCsrf, undefined) + assert.strictEqual(response2.accessToken, '') + assert.strictEqual(response2.refreshToken, '') + assert.strictEqual(response2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.accessTokenDomain, undefined) + assert.strictEqual(response2.refreshTokenDomain, undefined) + assert.strictEqual(response2.frontToken, 'remove') + + const response3 = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response3.text).status === 'OK') + assert(response3.status === 200) + + const res2 = extractInfoFromResponse(response3) + + const response4 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + assert.strictEqual(response4.antiCsrf, undefined) + assert.strictEqual(response4.accessToken, '') + assert.strictEqual(response4.refreshToken, '') + assert.strictEqual(response4.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response4.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response4.accessTokenDomain, undefined) + assert.strictEqual(response4.refreshTokenDomain, undefined) + assert.strictEqual(response4.frontToken, 'remove') + }) + + it('test that disabling default route and calling the API returns 404', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPasswordRecipe.init({ + providers: [customProvider1], + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'thirdpartyemailpassword') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 404) + }) + + it('test that calling the API without a session should return OK', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPasswordRecipe.init({ + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.body.status, 'OK') + assert.strictEqual(response.status, 200) + assert.strictEqual(response.header['set-cookie'], undefined) + }) + + it('test that signout API reutrns try refresh token, refresh session and signout should return OK', async () => { + await setKeyValueInConfig('access_token_validity', 2) + + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPasswordRecipe.init({ + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.statusCode, 200) + + const res1 = extractInfoFromResponse(response1) + + await new Promise(r => setTimeout(r, 5000)) + + let signOutResponse = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${res1.accessToken}`]) + .set('anti-csrf', res1.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(signOutResponse.status, 401) + assert.strictEqual(signOutResponse.body.message, 'try refresh token') + + let refreshedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .expect(200) + .set('rid', 'session') + .set('Cookie', [`sRefreshToken=${res1.refreshToken}`]) + .set('anti-csrf', res1.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + signOutResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${refreshedResponse.accessToken}`]) + .set('anti-csrf', refreshedResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(signOutResponse.antiCsrf, undefined) + assert.strictEqual(signOutResponse.accessToken, '') + assert.strictEqual(signOutResponse.refreshToken, '') + assert.strictEqual(signOutResponse.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(signOutResponse.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(signOutResponse.accessTokenDomain, undefined) + assert.strictEqual(signOutResponse.refreshTokenDomain, undefined) + const response2 = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response2.text).status === 'OK') + assert(response2.status === 200) + + const res2 = extractInfoFromResponse(response2) + + await new Promise(r => setTimeout(r, 5000)) + + signOutResponse = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${res2.accessToken}`]) + .set('anti-csrf', res2.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(signOutResponse.status === 401) + assert(JSON.parse(signOutResponse.text).message === 'try refresh token') + + refreshedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('rid', 'session') + .set('Cookie', [`sRefreshToken=${res2.refreshToken}`]) + .set('anti-csrf', res2.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + signOutResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${refreshedResponse.accessToken}`]) + .set('anti-csrf', refreshedResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert(signOutResponse.antiCsrf === undefined) + assert(signOutResponse.accessToken === '') + assert(signOutResponse.refreshToken === '') + assert(signOutResponse.accessTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(signOutResponse.refreshTokenExpiry === 'Thu, 01 Jan 1970 00:00:00 GMT') + assert(signOutResponse.accessTokenDomain === undefined) + assert(signOutResponse.refreshTokenDomain === undefined) + }) +}) diff --git a/test/thirdpartyemailpassword/signupFeature.test.js b/test/thirdpartyemailpassword/signupFeature.test.js deleted file mode 100644 index c8336c228..000000000 --- a/test/thirdpartyemailpassword/signupFeature.test.js +++ /dev/null @@ -1,934 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, signUPRequest } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyEmailPasswordRecipe = require("../../lib/build/recipe/thirdpartyemailpassword/recipe").default; -let ThirdPartyEmailPassword = require("../../lib/build/recipe/thirdpartyemailpassword"); -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -const EmailVerification = require("../../recipe/emailverification"); -let { Querier } = require("../../lib/build/querier"); -let { maxVersion } = require("../../lib/build/utils"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signupTest: ${printPath("[test/thirdpartyemailpassword/signupFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider2 = { - id: "custom", - get: (recipe, authCode) => { - throw new Error("error from get function"); - }, - }; - this.customProvider3 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider4 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - throw new Error("error from getProfileInfo"); - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider5 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: false, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test that disable api, the default signinup API does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: undefined, - }; - }, - }, - providers: [ - ThirdPartyEmailPassword.Google({ - clientId: "test", - clientSecret: "test-secret", - }), - ], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "google", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.status, 404); - }); - - it("test that if disable api, the default signup API does not work", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailPasswordSignUpPOST: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(response.status === 404); - }); - - it("test minimum config with one provider", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").times(1).reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); - - assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), true); - }); - - it("test signUpAPI works when input is fine", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userInfo = JSON.parse(response.text).user; - assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); - }); - - it("test handlePostSignUpIn gets set correctly", async function () { - await startST(); - - process.env.userId = ""; - process.env.loginType = ""; - - assert.strictEqual(process.env.userId, ""); - assert.strictEqual(process.env.loginType, ""); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: async (input) => { - let response = await oI.thirdPartySignInUpPOST(input); - if (response.status === "OK") { - process.env.userId = response.user.id; - process.env.loginType = "thirdparty"; - } - return response; - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").times(1).reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.strictEqual(process.env.userId, response1.body.user.id); - assert.strictEqual(process.env.loginType, "thirdparty"); - }); - - it("test handlePostSignUp gets set correctly", async function () { - await startST(); - - process.env.userId = ""; - process.env.loginType = ""; - - assert.strictEqual(process.env.userId, ""); - assert.strictEqual(process.env.loginType, ""); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - override: { - apis: (oI) => { - return { - ...oI, - emailPasswordSignUpPOST: async (input) => { - let response = await oI.emailPasswordSignUpPOST(input); - if (response.status === "OK") { - process.env.userId = response.user.id; - process.env.loginType = "emailpassword"; - } - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userInfo = JSON.parse(response.text).user; - assert(userInfo.id !== undefined); - assert.strictEqual(process.env.userId, userInfo.id); - assert.strictEqual(process.env.loginType, "emailpassword"); - }); - - // will test that the error is correctly propagated to the required sub-recipe - it("test signUpAPI throws an error in case of a duplicate email (emailpassword)", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(JSON.parse(response.text).status === "OK"); - assert(response.status === 200); - - let userInfo = JSON.parse(response.text).user; - assert(userInfo.id !== undefined); - assert(userInfo.email === "random@gmail.com"); - - response = await signUPRequest(app, "random@gmail.com", "validpass123"); - assert(response.status === 200); - let responseInfo = JSON.parse(response.text); - - assert(responseInfo.status === "FIELD_ERROR"); - assert(responseInfo.formFields.length === 1); - assert(responseInfo.formFields[0].id === "email"); - assert(responseInfo.formFields[0].error === "This email already exists. Please sign in instead."); - }); - - // testing 500 status response thrown from sub-recipe - it("test provider get function throws error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyEmailPassword.init({ - providers: [this.customProvider2], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from get function" }); - }); - - // NO_EMAIL_GIVEN_BY_PROVIDER thrown from sub recipe - it("test email not returned in getProfileInfo function", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyEmailPassword.init({ - providers: [this.customProvider3], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 200); - assert.strictEqual(response1.body.status, "NO_EMAIL_GIVEN_BY_PROVIDER"); - }); - - it("test error thrown from getProfileInfo function", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyEmailPassword.init({ - providers: [this.customProvider4], - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from getProfileInfo" }); - }); - - it("test getUserById when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdPartyEmailPassword.getUserById("randomID"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyEmailPassword.getUserById(signUpUserInfo.id); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); - - it("test getUserByThirdPartyInfo when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdPartyEmailPassword.getUserByThirdPartyInfo("custom", "user"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyEmailPassword.getUserByThirdPartyInfo("custom", "user"); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); - - it("test getUserCount and pagination works fine", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - let currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - if (maxVersion(currCDIVersion, "2.7") === "2.7") { - // we don't run the tests below for older versions of the core since it - // was introduced in >= 2.8 CDI - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - assert((await STExpress.getUserCount()) === 0); - - await signUPRequest(app, "random@gmail.com", "validpass123"); - - assert((await STExpress.getUserCount()) === 1); - assert((await STExpress.getUserCount(["emailpassword"])) === 1); - assert((await STExpress.getUserCount(["emailpassword", "thirdparty"])) === 1); - - await ThirdPartyEmailPassword.thirdPartySignInUp("google", "randomUserId", "test@example.com"); - - assert((await STExpress.getUserCount()) === 2); - assert((await STExpress.getUserCount(["emailpassword"])) === 1); - assert((await STExpress.getUserCount(["thirdparty"])) === 1); - assert((await STExpress.getUserCount(["emailpassword", "thirdparty"])) === 2); - - await signUPRequest(app, "random1@gmail.com", "validpass123"); - - let usersOldest = await STExpress.getUsersOldestFirst(); - assert(usersOldest.nextPaginationToken === undefined); - assert(usersOldest.users.length === 3); - assert(usersOldest.users[0].recipeId === "emailpassword"); - assert(usersOldest.users[0].user.email === "random@gmail.com"); - - let usersNewest = await STExpress.getUsersNewestFirst({ - limit: 2, - }); - assert(usersNewest.nextPaginationToken !== undefined); - assert(usersNewest.users.length === 2); - assert(usersNewest.users[0].recipeId === "emailpassword"); - assert(usersNewest.users[0].user.email === "random1@gmail.com"); - - let usersNewest2 = await STExpress.getUsersNewestFirst({ - paginationToken: usersNewest.nextPaginationToken, - }); - assert(usersNewest2.nextPaginationToken === undefined); - assert(usersNewest2.users.length === 1); - assert(usersNewest2.users[0].recipeId === "emailpassword"); - assert(usersNewest2.users[0].user.email === "random@gmail.com"); - }); - - it("updateEmailOrPassword function test for third party login", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPassword.init({ - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - let thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); - - assert.strictEqual(await ThirdPartyEmailPassword.getUserByThirdPartyInfo("custom", "user"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - { - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyEmailPassword.getUserByThirdPartyInfo("custom", "user"); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - - try { - await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: userInfo.id, - email: "test2@example.com", - }); - throw new Error("test failed"); - } catch (err) { - if ( - err.message !== "Cannot update email or password of a user who signed up using third party login." - ) { - throw err; - } - } - } - - { - let response = await new Promise((resolve) => - request(app) - .post("/auth/signup") - .send({ - formFields: [ - { - id: "email", - value: "test@example.com", - }, - { - id: "password", - value: "pass@123", - }, - ], - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - - let r = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: signUpUserInfo.id, - email: "test2@example.com", - password: "haha@1234", - }); - - assert(r.status === "OK"); - - let r2 = await ThirdPartyEmailPassword.updateEmailOrPassword({ - userId: signUpUserInfo.id + "123", - email: "test2@example.com", - }); - - assert(r2.status === "UNKNOWN_USER_ID_ERROR"); - } - }); -}); diff --git a/test/thirdpartyemailpassword/signupFeature.test.ts b/test/thirdpartyemailpassword/signupFeature.test.ts new file mode 100644 index 000000000..dd8bcb096 --- /dev/null +++ b/test/thirdpartyemailpassword/signupFeature.test.ts @@ -0,0 +1,939 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyEmailPasswordRecipe from 'supertokens-node/recipe/thirdpartyemailpassword/recipe' +import ThirdPartyEmailPassword, { TypeProvider } from 'supertokens-node/recipe/thirdpartyemailpassword' +import nock from 'nock' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, signUPRequest, startST } from '../utils' + +describe(`signupTest: ${printPath('[test/thirdpartyemailpassword/signupFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + let customProvider2: TypeProvider + let customProvider3: TypeProvider + let customProvider4: TypeProvider + let customProvider5: TypeProvider + beforeEach(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider2 = { + id: 'custom', + get: (recipe, authCode) => { + throw new Error('error from get function') + }, + } + customProvider3 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider4 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + throw new Error('error from getProfileInfo') + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider5 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: false, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test that disable api, the default signinup API does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: undefined, + } + }, + }, + providers: [ + ThirdPartyEmailPassword.Google({ + clientId: 'test', + clientSecret: 'test-secret', + }), + ], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'google', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.status, 404) + }) + + it('test that if disable api, the default signup API does not work', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + emailPasswordSignUpPOST: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(response.status === 404) + }) + + it('test minimum config with one provider', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyEmailPassword.init({ + providers: [customProvider1], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').times(1).reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.createdNewUser, true) + assert.strictEqual(response1.body.user.thirdParty.id, 'custom') + assert.strictEqual(response1.body.user.thirdParty.userId, 'user') + assert.strictEqual(response1.body.user.email, 'email@test.com') + + assert.strictEqual(await EmailVerification.isEmailVerified(response1.body.user.id), true) + }) + + it('test signUpAPI works when input is fine', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userInfo = JSON.parse(response.text).user + assert(userInfo.id !== undefined) + assert(userInfo.email === 'random@gmail.com') + }) + + it('test handlePostSignUpIn gets set correctly', async () => { + await startST() + + process.env.userId = '' + process.env.loginType = '' + + assert.strictEqual(process.env.userId, '') + assert.strictEqual(process.env.loginType, '') + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyEmailPassword.init({ + providers: [customProvider1], + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: async (input) => { + const response = await oI.thirdPartySignInUpPOST(input) + if (response.status === 'OK') { + process.env.userId = response.user.id + process.env.loginType = 'thirdparty' + } + return response + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').times(1).reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.strictEqual(process.env.userId, response1.body.user.id) + assert.strictEqual(process.env.loginType, 'thirdparty') + }) + + it('test handlePostSignUp gets set correctly', async () => { + await startST() + + process.env.userId = '' + process.env.loginType = '' + + assert.strictEqual(process.env.userId, '') + assert.strictEqual(process.env.loginType, '') + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + emailPasswordSignUpPOST: async (input) => { + const response = await oI.emailPasswordSignUpPOST(input) + if (response.status === 'OK') { + process.env.userId = response.user.id + process.env.loginType = 'emailpassword' + } + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userInfo = JSON.parse(response.text).user + assert(userInfo.id !== undefined) + assert.strictEqual(process.env.userId, userInfo.id) + assert.strictEqual(process.env.loginType, 'emailpassword') + }) + + // will test that the error is correctly propagated to the required sub-recipe + it('test signUpAPI throws an error in case of a duplicate email (emailpassword)', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + let response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(JSON.parse(response.text).status === 'OK') + assert(response.status === 200) + + const userInfo = JSON.parse(response.text).user + assert(userInfo.id !== undefined) + assert(userInfo.email === 'random@gmail.com') + + response = await signUPRequest(app, 'random@gmail.com', 'validpass123') + assert(response.status === 200) + const responseInfo = JSON.parse(response.text) + + assert(responseInfo.status === 'FIELD_ERROR') + assert(responseInfo.formFields.length === 1) + assert(responseInfo.formFields[0].id === 'email') + assert(responseInfo.formFields[0].error === 'This email already exists. Please sign in instead.') + }) + + // testing 500 status response thrown from sub-recipe + it('test provider get function throws error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyEmailPassword.init({ + providers: [customProvider2], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from get function' }) + }) + + // NO_EMAIL_GIVEN_BY_PROVIDER thrown from sub recipe + it('test email not returned in getProfileInfo function', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyEmailPassword.init({ + providers: [customProvider3], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 200) + assert.strictEqual(response1.body.status, 'NO_EMAIL_GIVEN_BY_PROVIDER') + }) + + it('test error thrown from getProfileInfo function', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyEmailPassword.init({ + providers: [customProvider4], + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from getProfileInfo' }) + }) + + it('test getUserById when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + + assert.strictEqual(await ThirdPartyEmailPassword.getUserById('randomID'), undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + const userInfo = await ThirdPartyEmailPassword.getUserById(signUpUserInfo.id) + + assert.strictEqual(userInfo.email, signUpUserInfo.email) + assert.strictEqual(userInfo.id, signUpUserInfo.id) + }) + + it('test getUserByThirdPartyInfo when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + + assert.strictEqual(await ThirdPartyEmailPassword.getUserByThirdPartyInfo('custom', 'user'), undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + const userInfo = await ThirdPartyEmailPassword.getUserByThirdPartyInfo('custom', 'user') + + assert.strictEqual(userInfo.email, signUpUserInfo.email) + assert.strictEqual(userInfo.id, signUpUserInfo.id) + }) + + it('test getUserCount and pagination works fine', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ThirdPartyEmailPassword.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + if (maxVersion(currCDIVersion, '2.7') === '2.7') { + // we don't run the tests below for older versions of the core since it + // was introduced in >= 2.8 CDI + return + } + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + assert((await STExpress.getUserCount()) === 0) + + await signUPRequest(app, 'random@gmail.com', 'validpass123') + + assert((await STExpress.getUserCount()) === 1) + assert((await STExpress.getUserCount(['emailpassword'])) === 1) + assert((await STExpress.getUserCount(['emailpassword', 'thirdparty'])) === 1) + + await ThirdPartyEmailPassword.thirdPartySignInUp('google', 'randomUserId', 'test@example.com') + + assert((await STExpress.getUserCount()) === 2) + assert((await STExpress.getUserCount(['emailpassword'])) === 1) + assert((await STExpress.getUserCount(['thirdparty'])) === 1) + assert((await STExpress.getUserCount(['emailpassword', 'thirdparty'])) === 2) + + await signUPRequest(app, 'random1@gmail.com', 'validpass123') + + const usersOldest = await STExpress.getUsersOldestFirst() + assert(usersOldest.nextPaginationToken === undefined) + assert(usersOldest.users.length === 3) + assert(usersOldest.users[0].recipeId === 'emailpassword') + assert(usersOldest.users[0].user.email === 'random@gmail.com') + + const usersNewest = await STExpress.getUsersNewestFirst({ + limit: 2, + }) + assert(usersNewest.nextPaginationToken !== undefined) + assert(usersNewest.users.length === 2) + assert(usersNewest.users[0].recipeId === 'emailpassword') + assert(usersNewest.users[0].user.email === 'random1@gmail.com') + + const usersNewest2 = await STExpress.getUsersNewestFirst({ + paginationToken: usersNewest.nextPaginationToken, + }) + assert(usersNewest2.nextPaginationToken === undefined) + assert(usersNewest2.users.length === 1) + assert(usersNewest2.users[0].recipeId === 'emailpassword') + assert(usersNewest2.users[0].user.email === 'random@gmail.com') + }) + + it('updateEmailOrPassword function test for third party login', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPassword.init({ + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + const thirdPartyRecipe = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError() + + assert.strictEqual(await ThirdPartyEmailPassword.getUserByThirdPartyInfo('custom', 'user'), undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + { + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + const userInfo = await ThirdPartyEmailPassword.getUserByThirdPartyInfo('custom', 'user') + + assert.strictEqual(userInfo.email, signUpUserInfo.email) + assert.strictEqual(userInfo.id, signUpUserInfo.id) + + try { + await ThirdPartyEmailPassword.updateEmailOrPassword({ + userId: userInfo.id, + email: 'test2@example.com', + }) + throw new Error('test failed') + } + catch (err) { + if ( + err.message !== 'Cannot update email or password of a user who signed up using third party login.' + ) + throw err + } + } + + { + const response = await new Promise(resolve => + request(app) + .post('/auth/signup') + .send({ + formFields: [ + { + id: 'email', + value: 'test@example.com', + }, + { + id: 'password', + value: 'pass@123', + }, + ], + }) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + + const r = await ThirdPartyEmailPassword.updateEmailOrPassword({ + userId: signUpUserInfo.id, + email: 'test2@example.com', + password: 'haha@1234', + }) + + assert(r.status === 'OK') + + const r2 = await ThirdPartyEmailPassword.updateEmailOrPassword({ + userId: `${signUpUserInfo.id}123`, + email: 'test2@example.com', + }) + + assert(r2.status === 'UNKNOWN_USER_ID_ERROR') + } + }) +}) diff --git a/test/thirdpartypasswordless/api.test.js b/test/thirdpartypasswordless/api.test.js deleted file mode 100644 index 55e8b85d7..000000000 --- a/test/thirdpartypasswordless/api.test.js +++ /dev/null @@ -1,1536 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig, stopST } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let ThirdPartyPasswordless = require("../../recipe/thirdpartypasswordless"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let SuperTokens = require("../../lib/build/supertokens").default; -const request = require("supertest"); -const express = require("express"); -let { middleware, errorHandler } = require("../../framework/express"); -let { isCDIVersionCompatible } = require("../utils"); - -describe(`apisFunctions: ${printPath("[test/thirdpartypasswordless/apis.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - finish full sign up / in flow with email (create code -> consume code) - */ - - it("test for thirdPartyPasswordless, the sign up /in flow with email using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - createAndSendCustomEmail: (input) => { - userInputCode = input.userInputCode; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with email - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - - // consumeCode API - let validUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: validCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 3); - assert(Object.keys(validUserInputCodeResponse).length === 3); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - finish full sign up / in flow with phoneNumber (create code -> consume code) - */ - - it("test for thirdPartyPasswordless, the sign up /in flow with phoneNumber using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - userInputCode = input.userInputCode; - return; - }, - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with phoneNumber - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - - // consumeCode API - let validUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: validCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.phoneNumber === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 3); - assert(Object.keys(validUserInputCodeResponse).length === 3); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - create code with email and then resend code and make sure that sending email function is called while resending code - */ - it("test for thirdPartyPasswordless creating a code with email and then resending the code and check that the sending custom email function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomEmailCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - createAndSendCustomEmail: (input) => { - isCreateAndSendCustomEmailCalled = true; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with email - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(isCreateAndSendCustomEmailCalled); - - isCreateAndSendCustomEmailCalled = false; - - // resendCode API - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: validCreateCodeResponse.deviceId, - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - assert(isCreateAndSendCustomEmailCalled); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - create code with phone and then resend code and make sure that sending SMS function is called while resending code - */ - it("test with thirdPartyPasswordless, creating a code with phone and then resending the code and check that the sending custom SMS function is called while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomTextMessageCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - isCreateAndSendCustomTextMessageCalled = true; - return; - }, - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // createCodeAPI with email - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(isCreateAndSendCustomTextMessageCalled); - - isCreateAndSendCustomTextMessageCalled = false; - - // resendCode API - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: validCreateCodeResponse.deviceId, - preAuthSessionId: validCreateCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - assert(isCreateAndSendCustomTextMessageCalled); - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - sending both email and phone in createCode API throws bad request - * - sending neither email and phone in createCode API throws bad request - */ - it("test with thirdPartyPasswordless, invalid input to createCodeAPI while using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // sending both email and phone in createCode API throws bad request - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - email: "test@example.com", - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide exactly one of email or phoneNumber"); - } - - { - // sending neither email and phone in createCode API throws bad request - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.message === "Please provide exactly one of email or phoneNumber"); - } - }); - - /** - * - With contactMethod = EMAIL_OR_PHONE: - * - do full sign in with email, then manually add a user's phone to their user Info, then so sign in with that phone number and make sure that the same userId signs in. - - */ - - it("test with thirdPartyPasswordless, adding phoneNumber to a users info and signing in will sign in the same user, using the EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let userInputCode = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - userInputCode = input.userInputCode; - return; - }, - createAndSendCustomEmail: (input) => { - userInputCode = input.userInputCode; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // create a passwordless user with email - let emailCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(emailCreateCodeResponse.status === "OK"); - - // consumeCode API - let emailUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: emailCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: emailCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(emailUserInputCodeResponse.status === "OK"); - - // add users phoneNumber to userInfo - await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: emailUserInputCodeResponse.user.id, - phoneNumber: "+12345678901", - }); - - // sign in user with phone numbers - let phoneCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(phoneCreateCodeResponse.status === "OK"); - - let phoneUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: phoneCreateCodeResponse.preAuthSessionId, - userInputCode, - deviceId: phoneCreateCodeResponse.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(phoneUserInputCodeResponse.status === "OK"); - - // check that the same user has signed in - assert(phoneUserInputCodeResponse.user.id === emailUserInputCodeResponse.user.id); - }); - - // check that if user has not given linkCode nor (deviceId+userInputCode), it throws a bad request error. - it("test for thirdPartyPasswordless, not passing any fields to consumeCodeAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // dont send linkCode or (deviceId+userInputCode) - let badResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: "sessionId", - }) - .expect(400) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(badResponse.res.statusMessage === "Bad Request"); - assert( - JSON.parse(badResponse.text).message === - "Please provide one of (linkCode) or (deviceId+userInputCode) and not both" - ); - } - }); - - it("test with thirdPartyPasswordless consumeCodeAPI with magic link", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - { - // send an invalid linkCode - let letInvalidLinkCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: "invalidLinkCode", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(letInvalidLinkCodeResponse.status === "RESTART_FLOW_ERROR"); - } - - { - // send a valid linkCode - let validLinkCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validLinkCodeResponse.status === "OK"); - assert(validLinkCodeResponse.createdNewUser === true); - assert(typeof validLinkCodeResponse.user.id === "string"); - assert(typeof validLinkCodeResponse.user.email === "string"); - assert(typeof validLinkCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validLinkCodeResponse.user).length === 3); - assert(Object.keys(validLinkCodeResponse).length === 3); - } - }); - - it("test with thirdPartyPasswordless, consumeCodeAPI with code", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: () => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - { - // send an incorrect userInputCode - let incorrectUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: "invalidLinkCode", - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(incorrectUserInputCodeResponse.status === "INCORRECT_USER_INPUT_CODE_ERROR"); - assert(incorrectUserInputCodeResponse.failedCodeInputAttemptCount === 1); - //checking default value for maximumCodeInputAttempts is 5 - assert(incorrectUserInputCodeResponse.maximumCodeInputAttempts === 5); - assert(Object.keys(incorrectUserInputCodeResponse).length === 3); - } - - { - // send a valid userInputCode - let validUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validUserInputCodeResponse.status === "OK"); - assert(validUserInputCodeResponse.createdNewUser === true); - assert(typeof validUserInputCodeResponse.user.id === "string"); - assert(typeof validUserInputCodeResponse.user.email === "string"); - assert(typeof validUserInputCodeResponse.user.timeJoined === "number"); - assert(Object.keys(validUserInputCodeResponse.user).length === 3); - assert(Object.keys(validUserInputCodeResponse).length === 3); - } - - { - // send a used userInputCode - let usedUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(usedUserInputCodeResponse.status === "RESTART_FLOW_ERROR"); - } - }); - - it("test with thirdPartyPasswordless, consumeCodeAPI with expired code", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - await new Promise((r) => setTimeout(r, 2000)); // wait for code to expire - let expiredUserInputCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/consume") - .send({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(expiredUserInputCodeResponse.status === "EXPIRED_USER_INPUT_CODE_ERROR"); - assert(expiredUserInputCodeResponse.failedCodeInputAttemptCount === 1); - //checking default value for maximumCodeInputAttempts is 5 - assert(expiredUserInputCodeResponse.maximumCodeInputAttempts === 5); - assert(Object.keys(expiredUserInputCodeResponse).length === 3); - } - }); - - it("test with thirdPartyPasswordless, createCodeAPI with email", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // passing valid field - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - } - - { - // passing invalid email - let invalidEmailCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "invalidEmail", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(invalidEmailCreateCodeResponse.status === "GENERAL_ERROR"); - assert(invalidEmailCreateCodeResponse.message === "Email is invalid"); - } - }); - - it("test with thirdPartyPasswordless, createCodeAPI with phoneNumber", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // passing valid field - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(validCreateCodeResponse.status === "OK"); - assert(typeof validCreateCodeResponse.deviceId === "string"); - assert(typeof validCreateCodeResponse.preAuthSessionId === "string"); - assert(validCreateCodeResponse.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - assert(Object.keys(validCreateCodeResponse).length === 4); - } - - { - // passing invalid phoneNumber - let invalidPhoneNumberCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "123", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(invalidPhoneNumberCreateCodeResponse.status === "GENERAL_ERROR"); - assert(invalidPhoneNumberCreateCodeResponse.message === "Phone number is invalid"); - } - }); - - it("test with thirdPartyPasswordless, magicLink format in createCodeAPI", async function () { - await startST(); - - let magicLinkURL = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - magicLinkURL = new URL(input.urlWithLinkCode); - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // passing valid field - let validCreateCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(validCreateCodeResponse.status === "OK"); - - // check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode - assert(magicLinkURL.hostname === "supertokens.io"); - assert(magicLinkURL.pathname === "/auth/verify"); - assert(magicLinkURL.searchParams.get("rid") === "thirdpartypasswordless"); - assert(magicLinkURL.searchParams.get("preAuthSessionId") === validCreateCodeResponse.preAuthSessionId); - assert(magicLinkURL.hash.length > 1); - } - }); - - it("test with ThirdPartyPasswordless, emailExistsAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // email does not exist - let emailDoesNotExistResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(emailDoesNotExistResponse.status === "OK"); - assert(emailDoesNotExistResponse.exists === false); - } - - { - // email exists - - // create a passwordless user through email - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - }); - - let emailExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/email/exists") - .query({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(emailExistsResponse.status === "OK"); - assert(emailExistsResponse.exists === true); - } - }); - - it("test with thirdPartyPasswordless, phoneNumberExistsAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // phoneNumber does not exist - let phoneNumberDoesNotExistResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/phonenumber/exists") - .query({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(phoneNumberDoesNotExistResponse.status === "OK"); - assert(phoneNumberDoesNotExistResponse.exists === false); - } - - { - // phoneNumber exists - - // create a passwordless user through phone - let codeInfo = await ThirdPartyPasswordless.createCode({ - phoneNumber: "+1234567890", - }); - - await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - linkCode: codeInfo.linkCode, - }); - - let phoneNumberExistsResponse = await new Promise((resolve) => - request(app) - .get("/auth/signup/phonenumber/exists") - .query({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(phoneNumberExistsResponse.status === "OK"); - assert(phoneNumberExistsResponse.exists === true); - } - }); - - //resendCode API - - it("test with thirdPartyPasswordless, resendCodeAPI", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - let codeInfo = await ThirdPartyPasswordless.createCode({ - phoneNumber: "+1234567890", - }); - - // valid response - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: codeInfo.deviceId, - preAuthSessionId: codeInfo.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(response.status === "OK"); - } - - { - // invalid preAuthSessionId and deviceId - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: "TU/52WOcktSv99zqaAZuWJG9BSoS0aRLfCbep8rFEwk=", - preAuthSessionId: "kFmkPQEAJtACiT2w/K8fndEuNm+XozJXSZSlWEr+iGs=", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "RESTART_FLOW_ERROR"); - } - }); - - // test that you create a code with PHONE in config, you then change the config to use EMAIL, you call resendCode API, it should return RESTART_FLOW_ERROR - it("test with thirdPartyPasswordless, resendCodeAPI when changing contact method", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - let codeInfo = await ThirdPartyPasswordless.createCode({ - phoneNumber: "+1234567890", - }); - - await killAllST(); - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - { - // invalid preAuthSessionId and deviceId - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: codeInfo.deviceId, - preAuthSessionId: codeInfo.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "RESTART_FLOW_ERROR"); - } - } - }); -}); diff --git a/test/thirdpartypasswordless/api.test.ts b/test/thirdpartypasswordless/api.test.ts new file mode 100644 index 000000000..cd4a53fe2 --- /dev/null +++ b/test/thirdpartypasswordless/api.test.ts @@ -0,0 +1,1495 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import ThirdPartyPasswordless from 'supertokens-node/recipe/thirdpartypasswordless' +import { ProcessState } from 'supertokens-node/processState' +import request from 'supertest' +import express from 'express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, isCDIVersionCompatible, killAllST, printPath, setKeyValueInConfig, setupST, startST } from '../utils' + +describe(`apisFunctions: ${printPath('[test/thirdpartypasswordless/apis.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - finish full sign up / in flow with email (create code -> consume code) + */ + + it('test for thirdPartyPasswordless, the sign up /in flow with email using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with email + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + + // consumeCode API + const validUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: validCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validUserInputCodeResponse.status === 'OK') + assert(validUserInputCodeResponse.createdNewUser === true) + assert(typeof validUserInputCodeResponse.user.id === 'string') + assert(typeof validUserInputCodeResponse.user.email === 'string') + assert(typeof validUserInputCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validUserInputCodeResponse.user).length === 3) + assert(Object.keys(validUserInputCodeResponse).length === 3) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - finish full sign up / in flow with phoneNumber (create code -> consume code) + */ + + it('test for thirdPartyPasswordless, the sign up /in flow with phoneNumber using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + userInputCode = input.userInputCode + }, + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with phoneNumber + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + + // consumeCode API + const validUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: validCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validUserInputCodeResponse.status === 'OK') + assert(validUserInputCodeResponse.createdNewUser === true) + assert(typeof validUserInputCodeResponse.user.id === 'string') + assert(typeof validUserInputCodeResponse.user.phoneNumber === 'string') + assert(typeof validUserInputCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validUserInputCodeResponse.user).length === 3) + assert(Object.keys(validUserInputCodeResponse).length === 3) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - create code with email and then resend code and make sure that sending email function is called while resending code + */ + it('test for thirdPartyPasswordless creating a code with email and then resending the code and check that the sending custom email function is called while using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomEmailCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + createAndSendCustomEmail: (input) => { + isCreateAndSendCustomEmailCalled = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with email + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(isCreateAndSendCustomEmailCalled) + + isCreateAndSendCustomEmailCalled = false + + // resendCode API + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: validCreateCodeResponse.deviceId, + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + assert(isCreateAndSendCustomEmailCalled) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - create code with phone and then resend code and make sure that sending SMS function is called while resending code + */ + it('test with thirdPartyPasswordless, creating a code with phone and then resending the code and check that the sending custom SMS function is called while using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomTextMessageCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + isCreateAndSendCustomTextMessageCalled = true + }, + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // createCodeAPI with email + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(isCreateAndSendCustomTextMessageCalled) + + isCreateAndSendCustomTextMessageCalled = false + + // resendCode API + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: validCreateCodeResponse.deviceId, + preAuthSessionId: validCreateCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + assert(isCreateAndSendCustomTextMessageCalled) + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - sending both email and phone in createCode API throws bad request + * - sending neither email and phone in createCode API throws bad request + */ + it('test with thirdPartyPasswordless, invalid input to createCodeAPI while using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // sending both email and phone in createCode API throws bad request + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + email: 'test@example.com', + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide exactly one of email or phoneNumber') + } + + { + // sending neither email and phone in createCode API throws bad request + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.message === 'Please provide exactly one of email or phoneNumber') + } + }) + + /** + * - With contactMethod = EMAIL_OR_PHONE: + * - do full sign in with email, then manually add a user's phone to their user Info, then so sign in with that phone number and make sure that the same userId signs in. + + */ + + it('test with thirdPartyPasswordless, adding phoneNumber to a users info and signing in will sign in the same user, using the EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let userInputCode + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + userInputCode = input.userInputCode + }, + createAndSendCustomEmail: (input) => { + userInputCode = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // create a passwordless user with email + const emailCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(emailCreateCodeResponse.status === 'OK') + + // consumeCode API + const emailUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: emailCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: emailCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(emailUserInputCodeResponse.status === 'OK') + + // add users phoneNumber to userInfo + await ThirdPartyPasswordless.updatePasswordlessUser({ + userId: emailUserInputCodeResponse.user.id, + phoneNumber: '+12345678901', + }) + + // sign in user with phone numbers + const phoneCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(phoneCreateCodeResponse.status === 'OK') + + const phoneUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: phoneCreateCodeResponse.preAuthSessionId, + userInputCode, + deviceId: phoneCreateCodeResponse.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(phoneUserInputCodeResponse.status === 'OK') + + // check that the same user has signed in + assert(phoneUserInputCodeResponse.user.id === emailUserInputCodeResponse.user.id) + }) + + // check that if user has not given linkCode nor (deviceId+userInputCode), it throws a bad request error. + it('test for thirdPartyPasswordless, not passing any fields to consumeCodeAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // dont send linkCode or (deviceId+userInputCode) + const badResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: 'sessionId', + }) + .expect(400) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + assert(badResponse.res.statusMessage === 'Bad Request') + assert( + JSON.parse(badResponse.text).message + === 'Please provide one of (linkCode) or (deviceId+userInputCode) and not both', + ) + } + }) + + it('test with thirdPartyPasswordless consumeCodeAPI with magic link', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + { + // send an invalid linkCode + const letInvalidLinkCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: 'invalidLinkCode', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(letInvalidLinkCodeResponse.status === 'RESTART_FLOW_ERROR') + } + + { + // send a valid linkCode + const validLinkCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validLinkCodeResponse.status === 'OK') + assert(validLinkCodeResponse.createdNewUser === true) + assert(typeof validLinkCodeResponse.user.id === 'string') + assert(typeof validLinkCodeResponse.user.email === 'string') + assert(typeof validLinkCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validLinkCodeResponse.user).length === 3) + assert(Object.keys(validLinkCodeResponse).length === 3) + } + }) + + it('test with thirdPartyPasswordless, consumeCodeAPI with code', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: () => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + { + // send an incorrect userInputCode + const incorrectUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: 'invalidLinkCode', + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(incorrectUserInputCodeResponse.status === 'INCORRECT_USER_INPUT_CODE_ERROR') + assert(incorrectUserInputCodeResponse.failedCodeInputAttemptCount === 1) + // checking default value for maximumCodeInputAttempts is 5 + assert(incorrectUserInputCodeResponse.maximumCodeInputAttempts === 5) + assert(Object.keys(incorrectUserInputCodeResponse).length === 3) + } + + { + // send a valid userInputCode + const validUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validUserInputCodeResponse.status === 'OK') + assert(validUserInputCodeResponse.createdNewUser === true) + assert(typeof validUserInputCodeResponse.user.id === 'string') + assert(typeof validUserInputCodeResponse.user.email === 'string') + assert(typeof validUserInputCodeResponse.user.timeJoined === 'number') + assert(Object.keys(validUserInputCodeResponse.user).length === 3) + assert(Object.keys(validUserInputCodeResponse).length === 3) + } + + { + // send a used userInputCode + const usedUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(usedUserInputCodeResponse.status === 'RESTART_FLOW_ERROR') + } + }) + + it('test with thirdPartyPasswordless, consumeCodeAPI with expired code', async () => { + await setKeyValueInConfig('passwordless_code_lifetime', 1000) // one second lifetime + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + await new Promise(r => setTimeout(r, 2000)) // wait for code to expire + const expiredUserInputCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/consume') + .send({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(expiredUserInputCodeResponse.status === 'EXPIRED_USER_INPUT_CODE_ERROR') + assert(expiredUserInputCodeResponse.failedCodeInputAttemptCount === 1) + // checking default value for maximumCodeInputAttempts is 5 + assert(expiredUserInputCodeResponse.maximumCodeInputAttempts === 5) + assert(Object.keys(expiredUserInputCodeResponse).length === 3) + } + }) + + it('test with thirdPartyPasswordless, createCodeAPI with email', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // passing valid field + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + } + + { + // passing invalid email + const invalidEmailCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'invalidEmail', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(invalidEmailCreateCodeResponse.status === 'GENERAL_ERROR') + assert(invalidEmailCreateCodeResponse.message === 'Email is invalid') + } + }) + + it('test with thirdPartyPasswordless, createCodeAPI with phoneNumber', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // passing valid field + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(validCreateCodeResponse.status === 'OK') + assert(typeof validCreateCodeResponse.deviceId === 'string') + assert(typeof validCreateCodeResponse.preAuthSessionId === 'string') + assert(validCreateCodeResponse.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + assert(Object.keys(validCreateCodeResponse).length === 4) + } + + { + // passing invalid phoneNumber + const invalidPhoneNumberCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '123', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(invalidPhoneNumberCreateCodeResponse.status === 'GENERAL_ERROR') + assert(invalidPhoneNumberCreateCodeResponse.message === 'Phone number is invalid') + } + }) + + it('test with thirdPartyPasswordless, magicLink format in createCodeAPI', async () => { + await startST() + + let magicLinkURL + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + magicLinkURL = new URL(input.urlWithLinkCode) + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // passing valid field + const validCreateCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(validCreateCodeResponse.status === 'OK') + + // check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode + assert(magicLinkURL.hostname === 'supertokens.io') + assert(magicLinkURL.pathname === '/auth/verify') + assert(magicLinkURL.searchParams.get('rid') === 'thirdpartypasswordless') + assert(magicLinkURL.searchParams.get('preAuthSessionId') === validCreateCodeResponse.preAuthSessionId) + assert(magicLinkURL.hash.length > 1) + } + }) + + it('test with ThirdPartyPasswordless, emailExistsAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // email does not exist + const emailDoesNotExistResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(emailDoesNotExistResponse.status === 'OK') + assert(emailDoesNotExistResponse.exists === false) + } + + { + // email exists + + // create a passwordless user through email + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + }) + + const emailExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/email/exists') + .query({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(emailExistsResponse.status === 'OK') + assert(emailExistsResponse.exists === true) + } + }) + + it('test with thirdPartyPasswordless, phoneNumberExistsAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // phoneNumber does not exist + const phoneNumberDoesNotExistResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/phonenumber/exists') + .query({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(phoneNumberDoesNotExistResponse.status === 'OK') + assert(phoneNumberDoesNotExistResponse.exists === false) + } + + { + // phoneNumber exists + + // create a passwordless user through phone + const codeInfo = await ThirdPartyPasswordless.createCode({ + phoneNumber: '+1234567890', + }) + + await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + linkCode: codeInfo.linkCode, + }) + + const phoneNumberExistsResponse = await new Promise(resolve => + request(app) + .get('/auth/signup/phonenumber/exists') + .query({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(phoneNumberExistsResponse.status === 'OK') + assert(phoneNumberExistsResponse.exists === true) + } + }) + + // resendCode API + + it('test with thirdPartyPasswordless, resendCodeAPI', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + const codeInfo = await ThirdPartyPasswordless.createCode({ + phoneNumber: '+1234567890', + }) + + // valid response + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: codeInfo.deviceId, + preAuthSessionId: codeInfo.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + assert(response.status === 'OK') + } + + { + // invalid preAuthSessionId and deviceId + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: 'TU/52WOcktSv99zqaAZuWJG9BSoS0aRLfCbep8rFEwk=', + preAuthSessionId: 'kFmkPQEAJtACiT2w/K8fndEuNm+XozJXSZSlWEr+iGs=', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'RESTART_FLOW_ERROR') + } + }) + + // test that you create a code with PHONE in config, you then change the config to use EMAIL, you call resendCode API, it should return RESTART_FLOW_ERROR + it('test with thirdPartyPasswordless, resendCodeAPI when changing contact method', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + const codeInfo = await ThirdPartyPasswordless.createCode({ + phoneNumber: '+1234567890', + }) + + await killAllST() + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + { + // invalid preAuthSessionId and deviceId + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: codeInfo.deviceId, + preAuthSessionId: codeInfo.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'RESTART_FLOW_ERROR') + } + } + }) +}) diff --git a/test/thirdpartypasswordless/authorisationUrlFeature.test.js b/test/thirdpartypasswordless/authorisationUrlFeature.test.js deleted file mode 100644 index 72a7d88bc..000000000 --- a/test/thirdpartypasswordless/authorisationUrlFeature.test.js +++ /dev/null @@ -1,241 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, isCDIVersionCompatible } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyPasswordlessRecipe = require("../../lib/build/recipe/thirdpartypasswordless/recipe").default; -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`authorisationTest: ${printPath("[test/thirdpartyemailpassword/authorisationFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - params: { - scope: "test", - client_id: "supertokens", - }, - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - - this.customProvider2 = { - id: "custom", - get: (recipe, authCode) => { - throw new Error("error from get function"); - }, - getClientId: () => { - return "supertokens"; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test with thirdPartyPasswordless, minimum config for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyPasswordlessRecipe.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=custom") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.url, "https://test.com/oauth/auth?scope=test&client_id=supertokens"); - }); - - // testing 500 error thrown from sub-recipe - it("test with thirdPartyPasswordless, provider get function throws error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyPasswordlessRecipe.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider2], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=custom") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from get function" }); - }); - - // testing 4xx error correctly thrown from sub-recipe - it("test with thirdPartyPasswordless, thirdparty provider doesn't exist", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordlessRecipe.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .get("/auth/authorisationurl?thirdPartyId=google") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual( - response1.body.message, - "The third party provider google seems to be missing from the backend configs." - ); - }); -}); diff --git a/test/thirdpartypasswordless/authorisationUrlFeature.test.ts b/test/thirdpartypasswordless/authorisationUrlFeature.test.ts new file mode 100644 index 000000000..ba7b53365 --- /dev/null +++ b/test/thirdpartypasswordless/authorisationUrlFeature.test.ts @@ -0,0 +1,242 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyPasswordlessRecipe from 'supertokens-node/recipe/thirdpartypasswordless/recipe' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { TypeProvider } from 'supertokens-node/recipe/thirdpartypasswordless' +import { cleanST, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`authorisationTest: ${printPath('[test/thirdpartyemailpassword/authorisationFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + let customProvider2: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + params: { + scope: 'test', + client_id: 'supertokens', + }, + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + + customProvider2 = { + id: 'custom', + get: (recipe, authCode) => { + throw new Error('error from get function') + }, + getClientId: () => { + return 'supertokens' + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test with thirdPartyPasswordless, minimum config for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyPasswordlessRecipe.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=custom') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.url, 'https://test.com/oauth/auth?scope=test&client_id=supertokens') + }) + + // testing 500 error thrown from sub-recipe + it('test with thirdPartyPasswordless, provider get function throws error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyPasswordlessRecipe.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider2], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=custom') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from get function' }) + }) + + // testing 4xx error correctly thrown from sub-recipe + it('test with thirdPartyPasswordless, thirdparty provider doesn\'t exist', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordlessRecipe.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .get('/auth/authorisationurl?thirdPartyId=google') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual( + response1.body.message, + 'The third party provider google seems to be missing from the backend configs.', + ) + }) +}) diff --git a/test/thirdpartypasswordless/config.test.js b/test/thirdpartypasswordless/config.test.js deleted file mode 100644 index 938e69921..000000000 --- a/test/thirdpartypasswordless/config.test.js +++ /dev/null @@ -1,1585 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig, stopST, resetAll } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let ThirdPartyPasswordless = require("../../recipe/thirdpartypasswordless"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let SuperTokens = require("../../lib/build/supertokens").default; -const request = require("supertest"); -const express = require("express"); -let { middleware, errorHandler } = require("../../framework/express"); -let { isCDIVersionCompatible, generateRandomCode } = require("../utils"); -let ThirdPartyPasswordlessRecipe = require("../../lib/build/recipe/thirdpartypasswordless/recipe").default; - -describe(`config tests: ${printPath("[test/thirdpartypasswordless/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - /* - contactMethod: EMAIL_OR_PHONE - - minimal config - */ - it("test minimum config for thirdpartypasswordless with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let thirdPartyPasswordlessRecipe = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); - assert(thirdPartyPasswordlessRecipe.config.contactMethod === "EMAIL_OR_PHONE"); - assert(thirdPartyPasswordlessRecipe.config.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - }); - - /* - contactMethod: EMAIL_OR_PHONE - - adding custom validators for phone and email and making sure that they are called - */ - it("test for thirdPartyPasswordless, adding custom validators for phone and email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isValidateEmailAddressCalled = false; - let isValidatePhoneNumberCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - validateEmailAddress: (email) => { - isValidateEmailAddressCalled = true; - return undefined; - }, - validatePhoneNumber: (phoneNumber) => { - isValidatePhoneNumberCalled = true; - return undefined; - }, - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // check if validatePhoneNumber is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidatePhoneNumberCalled); - assert(response.status === "OK"); - } - - { - // check if validateEmailAddress is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidateEmailAddressCalled); - assert(response.status === "OK"); - } - }); - - /** - * - with contactMethod EMAIL_OR_PHONE - * - adding custom functions to send email and making sure that is called - */ - - it("test for thirdPartyPasswordless, use custom function to send email with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomEmailCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - isCreateAndSendCustomEmailCalled = true; - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // check if createAndSendCustomEmail is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isCreateAndSendCustomEmailCalled); - assert(response.status === "OK"); - } - }); - - /** - * - with contactMethod EMAIL_OR_PHONE - * - adding custom functions to send text SMS, and making sure that is called - * - */ - - it("test for thirdPartyPasswordless, use custom function to send text SMS with EMAIL_OR_PHONE contactMethod", async function () { - await startST(); - - let isCreateAndSendCustomTextMessageCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - isCreateAndSendCustomTextMessageCalled = true; - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // check if createAndSendCustomTextMessage is called - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isCreateAndSendCustomTextMessageCalled); - assert(response.status === "OK"); - } - }); - - /* - contactMethod: PHONE - - minimal input works - */ - it("test for thirdPartyPasswordless minimum config with phone contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let thirdPartyPasswordlessRecipe = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); - assert(thirdPartyPasswordlessRecipe.config.contactMethod === "PHONE"); - assert(thirdPartyPasswordlessRecipe.config.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - }); - - /* contactMethod: PHONE - If passed validatePhoneNumber, it gets called when the createCode API is called - - If you return undefined from the function, the API works. - - If you return a string from the function, the API throws a GENERIC ERROR - */ - - it("test for thirdPartyPasswordless, if validatePhoneNumber is called with phone contactMethod", async function () { - await startST(); - - let isValidatePhoneNumberCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - validatePhoneNumber: (phoneNumber) => { - isValidatePhoneNumberCalled = true; - return undefined; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // If you return undefined from the function, the API works - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidatePhoneNumberCalled); - assert(response.status === "OK"); - } - - { - // If you return a string from the function, the API throws a GENERIC ERROR - - await killAllST(); - await startST(); - - isValidatePhoneNumberCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - validatePhoneNumber: (phoneNumber) => { - isValidatePhoneNumberCalled = true; - return "test error"; - }, - }), - ], - }); - - { - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+1234567890", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidatePhoneNumberCalled); - assert(response.status === "GENERAL_ERROR"); - assert(response.message === "test error"); - } - } - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined - - check all other inputs to this function are as expected - */ - - it("test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE and phone contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - let isOtherInputValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE", - createAndSendCustomTextMessage: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - - if ( - typeof input.codeLifetime === "number" && - typeof input.phoneNumber === "string" && - typeof input.preAuthSessionId === "string" - ) { - isOtherInputValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - assert(isOtherInputValid); - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined - */ - - it("test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: MAGIC_LINK and phone contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined - */ - it("test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and phone contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: PHONE - If passed createAndSendCustomTextMessage, it gets called with the right inputs: - - if you throw an error from this function, it should contain a general error in the response - */ - - it("test with thirdPartyPasswordless, createAndSendCustomTextMessage, if error is thrown, it should contain a general error in the response", async function () { - await startST(); - - let isCreateAndSendCustomTextMessageCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - isCreateAndSendCustomTextMessageCalled = true; - throw new Error("test message"); - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - phoneNumber: "+12345678901", - }) - .expect(500) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 500); - assert(message === "test message"); - assert(isCreateAndSendCustomTextMessageCalled); - }); - - /* - - contactMethod: EMAIL - - minimal input works - */ - - it("test with thirdPartyPasswordless, minimum config with email contactMethod", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let thirdPartyPasswordlessRecipe = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); - assert(thirdPartyPasswordlessRecipe.config.contactMethod === "EMAIL"); - assert(thirdPartyPasswordlessRecipe.config.flowType === "USER_INPUT_CODE_AND_MAGIC_LINK"); - }); - - /* - - contactMethod: EMAIL - - if passed validateEmailAddress, it gets called when the createCode API is called - - If you return undefined from the function, the API works. - - If you return a string from the function, the API throws a GENERIC ERROR - */ - - it("test for thirdPartyPasswordless, if validateEmailAddress is called with email contactMethod", async function () { - await startST(); - - let isValidateEmailAddressCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - validateEmailAddress: (email) => { - isValidateEmailAddressCalled = true; - return undefined; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - { - // If you return undefined from the function, the API works - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidateEmailAddressCalled); - assert(response.status === "OK"); - } - - { - // If you return a string from the function, the API throws a GENERIC ERROR - - await killAllST(); - await startST(); - - isValidateEmailAddressCalled = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - validateEmailAddress: (email) => { - isValidateEmailAddressCalled = true; - return "test error"; - }, - }), - ], - }); - - { - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(isValidateEmailAddressCalled); - assert(response.status === "GENERAL_ERROR"); - assert(response.message === "test error"); - } - } - }); - - /* - - contactMethod: EMAIL - - if passed createAndSendCustomEmail, it gets called with the right inputs: - - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined - - check all other inputs to this function are as expected - */ - - it("test for thirdPartyPasswordless, createAndSendCustomEmail with flowType: USER_INPUT_CODE and email contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - let isOtherInputValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - createAndSendCustomEmail: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - - if ( - typeof input.codeLifetime === "number" && - typeof input.email === "string" && - typeof input.preAuthSessionId === "string" - ) { - isOtherInputValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - assert(isOtherInputValid); - }); - - /* contactMethod: EMAIL - If passed createAndSendCustomEmail, it gets called with the right inputs: - - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined - */ - - it("test with thirdPartyPasswordless, createAndSendCustomEmail with flowType: MAGIC_LINK and email contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "MAGIC_LINK", - createAndSendCustomEmail: (input) => { - if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: EMAIL - If passed createAndSendCustomEmail, it gets called with the right inputs: - - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined - */ - it("test with ThirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and email contact method", async function () { - await startST(); - - let isUserInputCodeAndUrlWithLinkCodeValid = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) { - isUserInputCodeAndUrlWithLinkCodeValid = true; - } - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(response.status === "OK"); - assert(isUserInputCodeAndUrlWithLinkCodeValid); - }); - - /* contactMethod: EMAIL - If passed createAndSendCustomEmail, it gets called with the right inputs: - - if you throw an error from this function, the status in the response should be a general error - */ - - it("test for thirdPartyPasswordless, that for createAndSendCustomEmail, if error is thrown, the status in the response should be a general error", async function () { - await startST(); - - let isCreateAndSendCustomEmailCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "MAGIC_LINK", - createAndSendCustomEmail: (input) => { - isCreateAndSendCustomEmailCalled = true; - throw new Error("test message"); - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(500) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert(response.status === 500); - assert(message === "test message"); - assert(isCreateAndSendCustomEmailCalled); - }); - - /* - - Missing compulsory configs throws as error: - - flowType is necessary, contactMethod is necessary - */ - - it("test thirdPartyPasswordless, missing compulsory configs throws an error", async function () { - await startST(); - - { - // missing flowType - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - }), - ], - }); - assert(false); - } catch (err) { - if (err.message !== "Please pass flowType argument in the config") { - throw err; - } - } - } - - { - await killAllST(); - await startST(); - - // missing contactMethod - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - flowType: "USER_INPUT_CODE", - }), - ], - }); - assert(false); - } catch (err) { - if (err.message !== `Please pass one of "PHONE", "EMAIL" or "EMAIL_OR_PHONE" as the contactMethod`) { - throw err; - } - } - } - }); - - /* - - Passing getCustomUserInputCode: - - Check that it is called when the createCode and resendCode APIs are called - - Check that the result returned from this are actually what the user input code is - - Check that is you return the same code everytime from this function and call resendCode API, you get USER_INPUT_CODE_ALREADY_USED_ERROR output from the API - */ - - it("test for thirdPartyPasswordless, passing getCustomUserInputCode using different codes", async function () { - await startST(); - - let customCode = undefined; - let userCodeSent = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - getCustomUserInputCode: (input) => { - customCode = generateRandomCode(5); - return customCode; - }, - createAndSendCustomEmail: (input) => { - userCodeSent = input.userInputCode; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let createCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(createCodeResponse.status === "OK"); - - assert(userCodeSent === customCode); - - customCode = undefined; - userCodeSent = undefined; - - let resendUserCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: createCodeResponse.deviceId, - preAuthSessionId: createCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - assert(resendUserCodeResponse.status === "OK"); - assert(userCodeSent === customCode); - }); - - it("test for thirdPartyPasswordless, passing getCustomUserInputCode using the same code", async function () { - await startST(); - - // using the same customCode - let customCode = "customCode"; - let userCodeSent = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - getCustomUserInputCode: (input) => { - return customCode; - }, - createAndSendCustomEmail: (input) => { - userCodeSent = input.userInputCode; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let createCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(createCodeResponse.status === "OK"); - assert(userCodeSent === customCode); - - let createNewCodeForDeviceResponse = await ThirdPartyPasswordless.createNewCodeForDevice({ - deviceId: createCodeResponse.deviceId, - userInputCode: customCode, - }); - - assert(createNewCodeForDeviceResponse.status === "USER_INPUT_CODE_ALREADY_USED_ERROR"); - - let resendUserCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code/resend") - .send({ - deviceId: createCodeResponse.deviceId, - preAuthSessionId: createCodeResponse.preAuthSessionId, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(resendUserCodeResponse.status === "GENERAL_ERROR"); - assert(resendUserCodeResponse.message === "Failed to generate a one time code. Please try again"); - }); - - // Check basic override usage - it("test basic override usage in thirdPartyPasswordless", async function () { - await startST(); - - let customDeviceId = "customDeviceId"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - createAndSendCustomEmail: (input) => { - return; - }, - override: { - apis: (oI) => { - return { - ...oI, - createCodePOST: async (input) => { - let response = await oI.createCodePOST(input); - response.deviceId = customDeviceId; - return response; - }, - }; - }, - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let createCodeResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup/code") - .send({ - email: "test@example.com", - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(JSON.parse(res.text)); - } - }) - ); - - assert(createCodeResponse.deviceId === customDeviceId); - }); - - it("test for thirdPartyPasswordless, default config for thirdparty", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - providers: [ - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test", - }), - ], - }), - ], - }); - - let thirdPartyPasswordless = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError(); - let config = thirdPartyPasswordless.config; - - assert(config.providers.length === 1); - let provider = config.providers[0]; - assert(provider.id === "google"); - }); - - it("test for thirdPartyPasswordless, minimum config for thirdparty module, custom provider", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - providers: [ - { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "test.com/oauth/token", - }, - authorisationRedirect: { - url: "test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - }; - }, - }, - ], - }), - ], - }); - }); -}); diff --git a/test/thirdpartypasswordless/config.test.ts b/test/thirdpartypasswordless/config.test.ts new file mode 100644 index 000000000..ae949c4d5 --- /dev/null +++ b/test/thirdpartypasswordless/config.test.ts @@ -0,0 +1,1548 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import ThirdPartyPasswordless from 'supertokens-node/recipe/thirdpartypasswordless' +import { ProcessState } from 'supertokens-node/processState' +import request from 'supertest' +import express from 'express' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import ThirdPartyPasswordlessRecipe from 'supertokens-node/recipe/thirdpartypasswordless/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, generateRandomCode, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`config tests: ${printPath('[test/thirdpartypasswordless/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + /* + contactMethod: EMAIL_OR_PHONE + - minimal config + */ + it('test minimum config for thirdpartypasswordless with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const thirdPartyPasswordlessRecipe = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError() + assert(thirdPartyPasswordlessRecipe.config.contactMethod === 'EMAIL_OR_PHONE') + assert(thirdPartyPasswordlessRecipe.config.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + }) + + /* + contactMethod: EMAIL_OR_PHONE + - adding custom validators for phone and email and making sure that they are called + */ + it('test for thirdPartyPasswordless, adding custom validators for phone and email with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isValidateEmailAddressCalled = false + let isValidatePhoneNumberCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + validateEmailAddress: (email) => { + isValidateEmailAddressCalled = true + return undefined + }, + validatePhoneNumber: (phoneNumber) => { + isValidatePhoneNumberCalled = true + return undefined + }, + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // check if validatePhoneNumber is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidatePhoneNumberCalled) + assert(response.status === 'OK') + } + + { + // check if validateEmailAddress is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidateEmailAddressCalled) + assert(response.status === 'OK') + } + }) + + /** + * - with contactMethod EMAIL_OR_PHONE + * - adding custom functions to send email and making sure that is called + */ + + it('test for thirdPartyPasswordless, use custom function to send email with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomEmailCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + isCreateAndSendCustomEmailCalled = true + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // check if createAndSendCustomEmail is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isCreateAndSendCustomEmailCalled) + assert(response.status === 'OK') + } + }) + + /** + * - with contactMethod EMAIL_OR_PHONE + * - adding custom functions to send text SMS, and making sure that is called + * + */ + + it('test for thirdPartyPasswordless, use custom function to send text SMS with EMAIL_OR_PHONE contactMethod', async () => { + await startST() + + let isCreateAndSendCustomTextMessageCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + isCreateAndSendCustomTextMessageCalled = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // check if createAndSendCustomTextMessage is called + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isCreateAndSendCustomTextMessageCalled) + assert(response.status === 'OK') + } + }) + + /* + contactMethod: PHONE + - minimal input works + */ + it('test for thirdPartyPasswordless minimum config with phone contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const thirdPartyPasswordlessRecipe = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError() + assert(thirdPartyPasswordlessRecipe.config.contactMethod === 'PHONE') + assert(thirdPartyPasswordlessRecipe.config.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + }) + + /* contactMethod: PHONE + If passed validatePhoneNumber, it gets called when the createCode API is called + - If you return undefined from the function, the API works. + - If you return a string from the function, the API throws a GENERIC ERROR + */ + + it('test for thirdPartyPasswordless, if validatePhoneNumber is called with phone contactMethod', async () => { + await startST() + + let isValidatePhoneNumberCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + validatePhoneNumber: (phoneNumber) => { + isValidatePhoneNumberCalled = true + return undefined + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // If you return undefined from the function, the API works + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidatePhoneNumberCalled) + assert(response.status === 'OK') + } + + { + // If you return a string from the function, the API throws a GENERIC ERROR + + await killAllST() + await startST() + + isValidatePhoneNumberCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + validatePhoneNumber: (phoneNumber) => { + isValidatePhoneNumberCalled = true + return 'test error' + }, + }), + ], + }) + + { + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+1234567890', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidatePhoneNumberCalled) + assert(response.status === 'GENERAL_ERROR') + assert(response.message === 'test error') + } + } + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined + - check all other inputs to this function are as expected + */ + + it('test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE and phone contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + let isOtherInputValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE', + createAndSendCustomTextMessage: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + + if ( + typeof input.codeLifetime === 'number' + && typeof input.phoneNumber === 'string' + && typeof input.preAuthSessionId === 'string' + ) + isOtherInputValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + assert(isOtherInputValid) + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined + */ + + it('test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: MAGIC_LINK and phone contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined + */ + it('test for thirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and phone contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: PHONE + If passed createAndSendCustomTextMessage, it gets called with the right inputs: + - if you throw an error from this function, it should contain a general error in the response + */ + + it('test with thirdPartyPasswordless, createAndSendCustomTextMessage, if error is thrown, it should contain a general error in the response', async () => { + await startST() + + let isCreateAndSendCustomTextMessageCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + isCreateAndSendCustomTextMessageCalled = true + throw new Error('test message') + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + phoneNumber: '+12345678901', + }) + .expect(500) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 500) + assert(message === 'test message') + assert(isCreateAndSendCustomTextMessageCalled) + }) + + /* + - contactMethod: EMAIL + - minimal input works + */ + + it('test with thirdPartyPasswordless, minimum config with email contactMethod', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const thirdPartyPasswordlessRecipe = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError() + assert(thirdPartyPasswordlessRecipe.config.contactMethod === 'EMAIL') + assert(thirdPartyPasswordlessRecipe.config.flowType === 'USER_INPUT_CODE_AND_MAGIC_LINK') + }) + + /* + - contactMethod: EMAIL + - if passed validateEmailAddress, it gets called when the createCode API is called + - If you return undefined from the function, the API works. + - If you return a string from the function, the API throws a GENERIC ERROR + */ + + it('test for thirdPartyPasswordless, if validateEmailAddress is called with email contactMethod', async () => { + await startST() + + let isValidateEmailAddressCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + validateEmailAddress: (email) => { + isValidateEmailAddressCalled = true + return undefined + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + { + // If you return undefined from the function, the API works + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidateEmailAddressCalled) + assert(response.status === 'OK') + } + + { + // If you return a string from the function, the API throws a GENERIC ERROR + + await killAllST() + await startST() + + isValidateEmailAddressCalled = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + validateEmailAddress: (email) => { + isValidateEmailAddressCalled = true + return 'test error' + }, + }), + ], + }) + + { + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(isValidateEmailAddressCalled) + assert(response.status === 'GENERAL_ERROR') + assert(response.message === 'test error') + } + } + }) + + /* + - contactMethod: EMAIL + - if passed createAndSendCustomEmail, it gets called with the right inputs: + - flowType: USER_INPUT_CODE -> userInputCode !== undefined && urlWithLinkCode == undefined + - check all other inputs to this function are as expected + */ + + it('test for thirdPartyPasswordless, createAndSendCustomEmail with flowType: USER_INPUT_CODE and email contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + let isOtherInputValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + createAndSendCustomEmail: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode === undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + + if ( + typeof input.codeLifetime === 'number' + && typeof input.email === 'string' + && typeof input.preAuthSessionId === 'string' + ) + isOtherInputValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + assert(isOtherInputValid) + }) + + /* contactMethod: EMAIL + If passed createAndSendCustomEmail, it gets called with the right inputs: + - flowType: MAGIC_LINK -> userInputCode === undefined && urlWithLinkCode !== undefined + */ + + it('test with thirdPartyPasswordless, createAndSendCustomEmail with flowType: MAGIC_LINK and email contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'MAGIC_LINK', + createAndSendCustomEmail: (input) => { + if (input.userInputCode === undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: EMAIL + If passed createAndSendCustomEmail, it gets called with the right inputs: + - flowType: USER_INPUT_CODE_AND_MAGIC_LINK -> userInputCode !== undefined && urlWithLinkCode !== undefined + */ + it('test with ThirdPartyPasswordless, createAndSendCustomTextMessage with flowType: USER_INPUT_CODE_AND_MAGIC_LINK and email contact method', async () => { + await startST() + + let isUserInputCodeAndUrlWithLinkCodeValid = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + if (input.userInputCode !== undefined && input.urlWithLinkCode !== undefined) + isUserInputCodeAndUrlWithLinkCodeValid = true + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(response.status === 'OK') + assert(isUserInputCodeAndUrlWithLinkCodeValid) + }) + + /* contactMethod: EMAIL + If passed createAndSendCustomEmail, it gets called with the right inputs: + - if you throw an error from this function, the status in the response should be a general error + */ + + it('test for thirdPartyPasswordless, that for createAndSendCustomEmail, if error is thrown, the status in the response should be a general error', async () => { + await startST() + + let isCreateAndSendCustomEmailCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'MAGIC_LINK', + createAndSendCustomEmail: (input) => { + isCreateAndSendCustomEmailCalled = true + throw new Error('test message') + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(500) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert(response.status === 500) + assert(message === 'test message') + assert(isCreateAndSendCustomEmailCalled) + }) + + /* + - Missing compulsory configs throws as error: + - flowType is necessary, contactMethod is necessary + */ + + it('test thirdPartyPasswordless, missing compulsory configs throws an error', async () => { + await startST() + + { + // missing flowType + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + }), + ], + }) + assert(false) + } + catch (err) { + if (err.message !== 'Please pass flowType argument in the config') + throw err + } + } + + { + await killAllST() + await startST() + + // missing contactMethod + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + flowType: 'USER_INPUT_CODE', + }), + ], + }) + assert(false) + } + catch (err) { + if (err.message !== 'Please pass one of "PHONE", "EMAIL" or "EMAIL_OR_PHONE" as the contactMethod') + throw err + } + } + }) + + /* + - Passing getCustomUserInputCode: + - Check that it is called when the createCode and resendCode APIs are called + - Check that the result returned from this are actually what the user input code is + - Check that is you return the same code everytime from this function and call resendCode API, you get USER_INPUT_CODE_ALREADY_USED_ERROR output from the API + */ + + it('test for thirdPartyPasswordless, passing getCustomUserInputCode using different codes', async () => { + await startST() + + let customCode + let userCodeSent + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + getCustomUserInputCode: (input) => { + customCode = generateRandomCode(5) + return customCode + }, + createAndSendCustomEmail: (input) => { + userCodeSent = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const createCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(createCodeResponse.status === 'OK') + + assert(userCodeSent === customCode) + + customCode = undefined + userCodeSent = undefined + + const resendUserCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: createCodeResponse.deviceId, + preAuthSessionId: createCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + assert(resendUserCodeResponse.status === 'OK') + assert(userCodeSent === customCode) + }) + + it('test for thirdPartyPasswordless, passing getCustomUserInputCode using the same code', async () => { + await startST() + + // using the same customCode + const customCode = 'customCode' + let userCodeSent + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + getCustomUserInputCode: (input) => { + return customCode + }, + createAndSendCustomEmail: (input) => { + userCodeSent = input.userInputCode + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const createCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(createCodeResponse.status === 'OK') + assert(userCodeSent === customCode) + + const createNewCodeForDeviceResponse = await ThirdPartyPasswordless.createNewCodeForDevice({ + deviceId: createCodeResponse.deviceId, + userInputCode: customCode, + }) + + assert(createNewCodeForDeviceResponse.status === 'USER_INPUT_CODE_ALREADY_USED_ERROR') + + const resendUserCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code/resend') + .send({ + deviceId: createCodeResponse.deviceId, + preAuthSessionId: createCodeResponse.preAuthSessionId, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(resendUserCodeResponse.status === 'GENERAL_ERROR') + assert(resendUserCodeResponse.message === 'Failed to generate a one time code. Please try again') + }) + + // Check basic override usage + it('test basic override usage in thirdPartyPasswordless', async () => { + await startST() + + const customDeviceId = 'customDeviceId' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE', + createAndSendCustomEmail: (input) => { + + }, + override: { + apis: (oI) => { + return { + ...oI, + createCodePOST: async (input) => { + const response = await oI.createCodePOST(input) + response.deviceId = customDeviceId + return response + }, + } + }, + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const createCodeResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup/code') + .send({ + email: 'test@example.com', + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(JSON.parse(res.text)) + }), + ) + + assert(createCodeResponse.deviceId === customDeviceId) + }) + + it('test for thirdPartyPasswordless, default config for thirdparty', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + providers: [ + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test', + }), + ], + }), + ], + }) + + const thirdPartyPasswordless = await ThirdPartyPasswordlessRecipe.getInstanceOrThrowError() + const config = thirdPartyPasswordless.config + + assert(config.providers.length === 1) + const provider = config.providers[0] + assert(provider.id === 'google') + }) + + it('test for thirdPartyPasswordless, minimum config for thirdparty module, custom provider', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + providers: [ + { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'test.com/oauth/token', + }, + authorisationRedirect: { + url: 'test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + } + }, + }, + ], + }), + ], + }) + }) +}) diff --git a/test/thirdpartypasswordless/emailDelivery.test.js b/test/thirdpartypasswordless/emailDelivery.test.js deleted file mode 100644 index 79a72b331..000000000 --- a/test/thirdpartypasswordless/emailDelivery.test.js +++ /dev/null @@ -1,1396 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse, delay } = require("../utils"); -let STExpress = require("../.."); -const EmailVerification = require("../../recipe/emailverification"); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdpartyPasswordless = require("../../recipe/thirdpartypasswordless"); -let { SMTPService } = require("../../recipe/thirdpartypasswordless/emaildelivery"); -let nock = require("nock"); -let supertest = require("supertest"); -const { middleware, errorHandler } = require("../../framework/express"); -let express = require("express"); -let { isCDIVersionCompatible } = require("../utils"); - -describe(`emailDelivery: ${printPath("[test/thirdpartypasswordless/emailDelivery.test.js]")}`, function () { - before(function () { - this.customProvider = { - id: "supertokens", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: authCodeResponse.id, - email: { - id: authCodeResponse.email, - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await cleanST(); - }); - - it("test default backward compatibility api being called: email verify", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdpartyPasswordless.init({ - providers: [this.customProvider], - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdpartyPasswordless.thirdPartySignInUp("supertokens", "test-user-id", "test@example.com"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - let appName = undefined; - let email = undefined; - let emailVerifyURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - emailVerifyURL = body.emailVerifyURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test default backward compatibility api being called, error message not sent back to user: email verify", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdpartyPasswordless.init({ - providers: [this.customProvider], - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdpartyPasswordless.thirdPartySignInUp("supertokens", "test-user-id", "test@example.com"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - let appName = undefined; - let email = undefined; - let emailVerifyURL = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - emailVerifyURL = body.emailVerifyURL; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(emailVerifyURL, undefined); - assert.strictEqual(result.body.status, "OK"); - }); - - it("test backward compatibility: email verify (thirdparty user)", async function () { - await startST(); - let idInCallback = undefined; - let email = undefined; - let emailVerifyURL = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { - idInCallback = input.id; - email = input.email; - emailVerifyURL = emailVerificationURLWithToken; - tj = input.timeJoined; - }, - }), - ThirdpartyPasswordless.init({ - providers: [this.customProvider], - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdpartyPasswordless.thirdPartySignInUp("supertokens", "test-user-id", "test@example.com"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(idInCallback, user.user.id); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test backward compatibility: email verify (passwordless user)", async function () { - await startST(); - let functionCalled = false; - let email = undefined; - let emailVerifyURL = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdpartyPasswordless.init({ - providers: [this.customProvider], - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailVerificationFeature: { - createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { - functionCalled = true; - email = input.email; - emailVerifyURL = emailVerificationURLWithToken; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdpartyPasswordless.passwordlessSignInUp({ - email: "test@example.com", - }); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - await delay(2); - assert.strictEqual(functionCalled, false); - assert.strictEqual(email, undefined); - assert.strictEqual(emailVerifyURL, undefined); - }); - - it("test custom override: email verify", async function () { - await startST(); - let email = undefined; - let emailVerifyURL = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - email = input.user.email; - emailVerifyURL = input.emailVerifyLink; - type = input.type; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - ThirdpartyPasswordless.init({ - providers: [this.customProvider], - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdpartyPasswordless.thirdPartySignInUp("supertokens", "test-user-id", "test@example.com"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/email/verify") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "EMAIL_VERIFICATION"); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test smtp service: email verify", async function () { - await startST(); - let email = undefined; - let emailVerifyURL = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - sendRawEmailCalled = true; - assert.strictEqual(input.body, emailVerifyURL); - assert.strictEqual(input.subject, "custom subject"); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "EMAIL_VERIFICATION"); - emailVerifyURL = input.emailVerifyLink; - return { - body: input.emailVerifyLink, - toEmail: input.user.email, - subject: "custom subject", - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - ThirdpartyPasswordless.init({ - providers: [this.customProvider], - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.post("/create", async (req, res) => { - await Session.createNewSession(req, res, req.body.id, {}, {}); - res.status(200).send(""); - }); - app.use(errorHandler()); - - let user = await ThirdpartyPasswordless.thirdPartySignInUp("supertokens", "test-user-id", "test@example.com"); - let res = extractInfoFromResponse(await supertest(app).post("/create").send({ id: user.user.id }).expect(200)); - - await supertest(app) - .post("/auth/user/email/verify/token") - .set("rid", "emailverification") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.notStrictEqual(emailVerifyURL, undefined); - }); - - it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: passwordless login", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: async (input) => { - email = input.email; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: passwordless login", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - email = input.email; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test smtp service: passwordless login", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let urlWithLinkCode = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - providers: [this.customProvider], - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - sendRawEmailCalled = true; - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.subject, urlWithLinkCode); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - userInputCode = input.userInputCode; - urlWithLinkCode = input.urlWithLinkCode; - codeLifetime = input.codeLifetime; - return { - body: input.userInputCode, - toEmail: input.email, - subject: input.urlWithLinkCode, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); - - it("test default backward compatibility api being called: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: resend code api", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let sendCustomEmailCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendCustomEmailCalled) { - email = input.email; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - } - sendCustomEmailCalled = true; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - await delay(2); - assert.strictEqual(sendCustomEmailCalled, true); - assert.strictEqual(email, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: resend code api", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - let overrideCalled = false; - let loginCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - override: (oI) => { - return { - sendEmail: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (overrideCalled) { - email = input.email; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - } - overrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - assert.strictEqual(overrideCalled, true); - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - appName = body.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(email, "test@example.com"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test smtp service: resend code api", async function () { - await startST(); - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let sendRawEmailCalled = false; - let getContentCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - emailDelivery: { - service: new SMTPService({ - smtpSettings: { - host: "", - from: { - email: "", - name: "", - }, - password: "", - port: 465, - secure: true, - }, - override: (oI) => { - return { - sendRawEmail: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendRawEmailCalled) { - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.subject, urlWithLinkCode); - assert.strictEqual(input.toEmail, "test@example.com"); - email = input.toEmail; - } - sendRawEmailCalled = true; - }, - getContent: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (getContentCalled) { - userInputCode = input.userInputCode; - urlWithLinkCode = input.urlWithLinkCode; - codeLifetime = input.codeLifetime; - } - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - return { - body: input.userInputCode, - toEmail: input.email, - subject: input.urlWithLinkCode, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendEmail: async (input) => { - outerOverrideCalled = true; - await oI.sendEmail(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - await delay(2); - assert(getContentCalled); - assert(sendRawEmailCalled); - assert.strictEqual(email, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - await delay(2); - - assert.strictEqual(email, "test@example.com"); - assert(outerOverrideCalled); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let email = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - email: "test@example.com", - }) - .expect(200); - - assert.strictEqual(appName, undefined); - assert.strictEqual(email, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.io") - .post("/0/st/auth/passwordless/login") - .reply(500, (uri, body) => { - appName = body.appName; - email = body.email; - codeLifetime = body.codeLifetime; - urlWithLinkCode = body.urlWithLinkCode; - userInputCode = body.userInputCode; - return {}; - }); - - let result = await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(email, "test@example.com"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); -}); diff --git a/test/thirdpartypasswordless/emailDelivery.test.ts b/test/thirdpartypasswordless/emailDelivery.test.ts new file mode 100644 index 000000000..d273edda6 --- /dev/null +++ b/test/thirdpartypasswordless/emailDelivery.test.ts @@ -0,0 +1,1382 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import ThirdpartyPasswordless, { TypeProvider } from 'supertokens-node/recipe/thirdpartypasswordless' +import { SMTPService } from 'supertokens-node/recipe/thirdpartypasswordless/emaildelivery' +import nock from 'nock' +import supertest from 'supertest' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import express from 'express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { cleanST, delay, extractInfoFromResponse, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`emailDelivery: ${printPath('[test/thirdpartypasswordless/emailDelivery.test.ts]')}`, () => { + let customProvider: TypeProvider + beforeAll(() => { + customProvider = { + id: 'supertokens', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: authCodeResponse.id, + email: { + id: authCodeResponse.email, + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await cleanST() + }) + + it('test default backward compatibility api being called: email verify', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdpartyPasswordless.init({ + providers: [customProvider], + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdpartyPasswordless.thirdPartySignInUp('supertokens', 'test-user-id', 'test@example.com') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + let appName + let email + let emailVerifyURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + emailVerifyURL = body.emailVerifyURL + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test default backward compatibility api being called, error message not sent back to user: email verify', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdpartyPasswordless.init({ + providers: [customProvider], + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdpartyPasswordless.thirdPartySignInUp('supertokens', 'test-user-id', 'test@example.com') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + let appName + let email + let emailVerifyURL + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + emailVerifyURL = body.emailVerifyURL + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(emailVerifyURL, undefined) + assert.strictEqual(result.body.status, 'OK') + }) + + it('test backward compatibility: email verify (thirdparty user)', async () => { + await startST() + let idInCallback + let email + let emailVerifyURL + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { + idInCallback = input.id + email = input.email + emailVerifyURL = emailVerificationURLWithToken + tj = input.timeJoined + }, + }), + ThirdpartyPasswordless.init({ + providers: [customProvider], + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdpartyPasswordless.thirdPartySignInUp('supertokens', 'test-user-id', 'test@example.com') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(idInCallback, user.user.id) + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test backward compatibility: email verify (passwordless user)', async () => { + await startST() + let functionCalled = false + let email + let emailVerifyURL + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdpartyPasswordless.init({ + providers: [customProvider], + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailVerificationFeature: { + createAndSendCustomEmail: async (input, emailVerificationURLWithToken) => { + functionCalled = true + email = input.email + emailVerifyURL = emailVerificationURLWithToken + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdpartyPasswordless.passwordlessSignInUp({ + email: 'test@example.com', + }) + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + await delay(2) + assert.strictEqual(functionCalled, false) + assert.strictEqual(email, undefined) + assert.strictEqual(emailVerifyURL, undefined) + }) + + it('test custom override: email verify', async () => { + await startST() + let email + let emailVerifyURL + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + email = input.user.email + emailVerifyURL = input.emailVerifyLink + type = input.type + await oI.sendEmail(input) + }, + } + }, + }, + }), + ThirdpartyPasswordless.init({ + providers: [customProvider], + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdpartyPasswordless.thirdPartySignInUp('supertokens', 'test-user-id', 'test@example.com') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/email/verify') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'EMAIL_VERIFICATION') + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test smtp service: email verify', async () => { + await startST() + let email + let emailVerifyURL + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailVerification.init({ + mode: 'OPTIONAL', + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + sendRawEmailCalled = true + assert.strictEqual(input.body, emailVerifyURL) + assert.strictEqual(input.subject, 'custom subject') + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'EMAIL_VERIFICATION') + emailVerifyURL = input.emailVerifyLink + return { + body: input.emailVerifyLink, + toEmail: input.user.email, + subject: 'custom subject', + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + ThirdpartyPasswordless.init({ + providers: [customProvider], + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.post('/create', async (req, res) => { + await Session.createNewSession(req, res, req.body.id, {}, {}) + res.status(200).send('') + }) + app.use(errorHandler()) + + const user = await ThirdpartyPasswordless.thirdPartySignInUp('supertokens', 'test-user-id', 'test@example.com') + const res = extractInfoFromResponse(await supertest(app).post('/create').send({ id: user.user.id }).expect(200)) + + await supertest(app) + .post('/auth/user/email/verify/token') + .set('rid', 'emailverification') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.notStrictEqual(emailVerifyURL, undefined) + }) + + it('test default backward compatibility api being called: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: passwordless login', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: async (input) => { + email = input.email + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: passwordless login', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + email = input.email + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test smtp service: passwordless login', async () => { + await startST() + let email + let codeLifetime + let userInputCode + let urlWithLinkCode + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + providers: [customProvider], + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + sendRawEmailCalled = true + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.subject, urlWithLinkCode) + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + userInputCode = input.userInputCode + urlWithLinkCode = input.urlWithLinkCode + codeLifetime = input.codeLifetime + return { + body: input.userInputCode, + toEmail: input.email, + subject: input.urlWithLinkCode, + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) + + it('test default backward compatibility api being called: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: resend code api', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let sendCustomEmailCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendCustomEmailCalled) { + email = input.email + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + } + sendCustomEmailCalled = true + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + await delay(2) + assert.strictEqual(sendCustomEmailCalled, true) + assert.strictEqual(email, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: resend code api', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + let overrideCalled = false + let loginCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + override: (oI) => { + return { + sendEmail: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (overrideCalled) { + email = input.email + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + } + overrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + assert.strictEqual(overrideCalled, true) + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + appName = body.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(email, 'test@example.com') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test smtp service: resend code api', async () => { + await startST() + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let outerOverrideCalled = false + let sendRawEmailCalled = false + let getContentCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + host: '', + from: { + email: '', + name: '', + }, + password: '', + port: 465, + secure: true, + }, + override: (oI) => { + return { + sendRawEmail: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendRawEmailCalled) { + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.subject, urlWithLinkCode) + assert.strictEqual(input.toEmail, 'test@example.com') + email = input.toEmail + } + sendRawEmailCalled = true + }, + getContent: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (getContentCalled) { + userInputCode = input.userInputCode + urlWithLinkCode = input.urlWithLinkCode + codeLifetime = input.codeLifetime + } + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + return { + body: input.userInputCode, + toEmail: input.email, + subject: input.urlWithLinkCode, + } + }, + } + }, + }), + override: (oI) => { + return { + sendEmail: async (input) => { + outerOverrideCalled = true + await oI.sendEmail(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + await delay(2) + assert(getContentCalled) + assert(sendRawEmailCalled) + assert.strictEqual(email, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + await delay(2) + + assert.strictEqual(email, 'test@example.com') + assert(outerOverrideCalled) + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let email + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + email: 'test@example.com', + }) + .expect(200) + + assert.strictEqual(appName, undefined) + assert.strictEqual(email, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.io') + .post('/0/st/auth/passwordless/login') + .reply(500, (uri, body) => { + appName = body.appName + email = body.email + codeLifetime = body.codeLifetime + urlWithLinkCode = body.urlWithLinkCode + userInputCode = body.userInputCode + return {} + }) + + const result = await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(email, 'test@example.com') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) +}) diff --git a/test/thirdpartypasswordless/getUsersByEmailFeature.test.js b/test/thirdpartypasswordless/getUsersByEmailFeature.test.js deleted file mode 100644 index 35ee975ad..000000000 --- a/test/thirdpartypasswordless/getUsersByEmailFeature.test.js +++ /dev/null @@ -1,120 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, isCDIVersionCompatible } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyPasswordlessRecipe = require("../../lib/build/recipe/thirdpartypasswordless/recipe").default; -let ThirdPartyPasswordless = require("../../lib/build/recipe/thirdpartypasswordless"); -const { thirdPartySignInUp } = require("../../lib/build/recipe/thirdpartypasswordless"); -const { getUsersByEmail } = require("../../lib/build/recipe/thirdpartypasswordless"); -const { maxVersion } = require("../../lib/build/utils"); -let { Querier } = require("../../lib/build/querier"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`getUsersByEmail: ${printPath("[test/thirdpartypasswordless/getUsersByEmailFeature.test.js]")}`, function () { - const MockThirdPartyProvider = { - id: "mock", - }; - - const MockThirdPartyProvider2 = { - id: "mock2", - }; - - const testSTConfig = { - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [MockThirdPartyProvider], - }), - ], - }; - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("invalid email yields empty users array", async function () { - await startST(); - STExpress.init(testSTConfig); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - // given there are no users - - // when - const thirdPartyUsers = await getUsersByEmail("john.doe@example.com"); - - // then - assert.strictEqual(thirdPartyUsers.length, 0); - }); - - it("valid email yields third party users", async function () { - await startST(); - STExpress.init({ - ...testSTConfig, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [MockThirdPartyProvider, MockThirdPartyProvider2], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - await thirdPartySignInUp("mock", "thirdPartyJohnDoe", "john.doe@example.com"); - await thirdPartySignInUp("mock2", "thirdPartyDaveDoe", "john.doe@example.com"); - - const thirdPartyUsers = await getUsersByEmail("john.doe@example.com"); - - assert.strictEqual(thirdPartyUsers.length, 2); - - thirdPartyUsers.forEach((user) => { - assert.notStrictEqual(user.thirdParty.id, undefined); - assert.notStrictEqual(user.id, undefined); - assert.notStrictEqual(user.timeJoined, undefined); - assert.strictEqual(user.email, "john.doe@example.com"); - }); - }); -}); diff --git a/test/thirdpartypasswordless/getUsersByEmailFeature.test.ts b/test/thirdpartypasswordless/getUsersByEmailFeature.test.ts new file mode 100644 index 000000000..8c880de0f --- /dev/null +++ b/test/thirdpartypasswordless/getUsersByEmailFeature.test.ts @@ -0,0 +1,115 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyPasswordless, { TypeProvider, getUsersByEmail, thirdPartySignInUp } from 'supertokens-node/recipe/thirdpartypasswordless' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getUsersByEmail: ${printPath('[test/thirdpartypasswordless/getUsersByEmailFeature.test.ts]')}`, () => { + const MockThirdPartyProvider: TypeProvider = { + id: 'mock', + } + + const MockThirdPartyProvider2: TypeProvider = { + id: 'mock2', + } + + const testSTConfig = { + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [MockThirdPartyProvider], + }), + ], + } + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('invalid email yields empty users array', async () => { + await startST() + STExpress.init(testSTConfig) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + // given there are no users + + // when + const thirdPartyUsers = await getUsersByEmail('john.doe@example.com') + + // then + assert.strictEqual(thirdPartyUsers.length, 0) + }) + + it('valid email yields third party users', async () => { + await startST() + STExpress.init({ + ...testSTConfig, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [MockThirdPartyProvider, MockThirdPartyProvider2], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + await thirdPartySignInUp('mock', 'thirdPartyJohnDoe', 'john.doe@example.com') + await thirdPartySignInUp('mock2', 'thirdPartyDaveDoe', 'john.doe@example.com') + + const thirdPartyUsers = await getUsersByEmail('john.doe@example.com') + + assert.strictEqual(thirdPartyUsers.length, 2) + + thirdPartyUsers.forEach((user) => { + assert.notStrictEqual(user.thirdParty.id, undefined) + assert.notStrictEqual(user.id, undefined) + assert.notStrictEqual(user.timeJoined, undefined) + assert.strictEqual(user.email, 'john.doe@example.com') + }) + }) +}) diff --git a/test/thirdpartypasswordless/override.test.js b/test/thirdpartypasswordless/override.test.js deleted file mode 100644 index 81bde8791..000000000 --- a/test/thirdpartypasswordless/override.test.js +++ /dev/null @@ -1,558 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - stopST, - killAllST, - cleanST, - resetAll, - signUPRequest, - isCDIVersionCompatible, -} = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let SessionRecipe = require("../../lib/build/recipe/session/recipe").default; -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -const { Querier } = require("../../lib/build/querier"); -let ThirdPartyPasswordless = require("../../recipe/thirdpartypasswordless"); -const express = require("express"); -const request = require("supertest"); -let nock = require("nock"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`overrideTest: ${printPath("[test/thirdpartypasswordless/override.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("overriding functions tests", async function () { - await startST(); - let user = undefined; - let newUser = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - override: { - functions: (oI) => { - return { - ...oI, - thirdPartySignInUp: async (input) => { - let response = await oI.thirdPartySignInUp(input); - user = response.user; - newUser = response.createdNewUser; - return response; - }, - getUserById: async (input) => { - let response = await oI.getUserById(input); - user = response; - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdPartyPasswordless.getUserById(userId)); - }); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: signInResponse.user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(userByIdResponse, user); - }); - - it("overriding api tests", async function () { - await startST(); - let user = undefined; - let newUser = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: async (input) => { - let response = await oI.thirdPartySignInUpPOST(input); - if (response.status === "OK") { - user = response.user; - newUser = response.createdNewUser; - } - return response; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res) => { - let userId = req.query.userId; - res.json(await ThirdPartyPasswordless.getUserById(userId)); - }); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse.user, user); - - user = undefined; - assert.strictEqual(user, undefined); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse.user, user); - }); - - it("overriding functions tests, throws error", async function () { - await startST(); - let user = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - override: { - functions: (oI) => { - return { - ...oI, - thirdPartySignInUp: async (input) => { - let response = await oI.thirdPartySignInUp(input); - user = response.user; - newUser = response.createdNewUser; - if (newUser) { - throw { - error: "signup error", - }; - } - throw { - error: "signin error", - }; - }, - getUserById: async (input) => { - await oI.getUserById(input); - throw { - error: "get user error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.get("/user", async (req, res, next) => { - try { - let userId = req.query.userId; - res.json(await ThirdPartyPasswordless.getUserById(userId)); - } catch (err) { - next(err); - } - }); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.deepStrictEqual(signUpResponse, { error: "signup error", customError: true }); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - - let userByIdResponse = await new Promise((resolve) => - request(app) - .get("/user") - .query({ - userId: user.id, - }) - .expect(200) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.deepStrictEqual(userByIdResponse, { error: "get user error", customError: true }); - }); - - it("overriding api tests, throws error", async function () { - await startST(); - let user = undefined; - let newUser = undefined; - let emailExists = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - providers: [this.customProvider1], - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: async (input) => { - let response = await oI.thirdPartySignInUpPOST(input); - user = response.user; - newUser = response.createdNewUser; - if (newUser) { - throw { - error: "signup error", - }; - } - throw { - error: "signin error", - }; - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").times(2).reply(200, {}); - - app.use((err, req, res, next) => { - res.json({ - ...err, - customError: true, - }); - }); - - let signUpResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.notStrictEqual(user, undefined); - assert.strictEqual(newUser, true); - assert.deepStrictEqual(signUpResponse, { error: "signup error", customError: true }); - - let signInResponse = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(err.response.body); - } else { - resolve(res.body); - } - }) - ); - - assert.strictEqual(newUser, false); - assert.deepStrictEqual(signInResponse, { error: "signin error", customError: true }); - }); -}); diff --git a/test/thirdpartypasswordless/override.test.ts b/test/thirdpartypasswordless/override.test.ts new file mode 100644 index 000000000..f456c57cc --- /dev/null +++ b/test/thirdpartypasswordless/override.test.ts @@ -0,0 +1,553 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyPasswordless, { TypeProvider } from 'supertokens-node/recipe/thirdpartypasswordless' +import express from 'express' +import request from 'supertest' +import nock from 'nock' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + isCDIVersionCompatible, + killAllST, + printPath, + setupST, + startST, +} from '../utils' + +describe(`overrideTest: ${printPath('[test/thirdpartypasswordless/override.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('overriding functions tests', async () => { + await startST() + let user + let newUser + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + override: { + functions: (oI) => { + return { + ...oI, + thirdPartySignInUp: async (input) => { + const response = await oI.thirdPartySignInUp(input) + user = response.user + newUser = response.createdNewUser + return response + }, + getUserById: async (input) => { + const response = await oI.getUserById(input) + user = response + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await ThirdPartyPasswordless.getUserById(userId)) + }) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.deepStrictEqual(signUpResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, false) + assert.deepStrictEqual(signInResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: signInResponse.user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(userByIdResponse, user) + }) + + it('overriding api tests', async () => { + await startST() + let user + let newUser + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: async (input) => { + const response = await oI.thirdPartySignInUpPOST(input) + if (response.status === 'OK') { + user = response.user + newUser = response.createdNewUser + } + return response + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res) => { + const userId = req.query.userId + res.json(await ThirdPartyPasswordless.getUserById(userId)) + }) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.deepStrictEqual(signUpResponse.user, user) + + user = undefined + assert.strictEqual(user, undefined) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, false) + assert.deepStrictEqual(signInResponse.user, user) + }) + + it('overriding functions tests, throws error', async () => { + await startST() + let user + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + override: { + functions: (oI) => { + return { + ...oI, + thirdPartySignInUp: async (input) => { + const response = await oI.thirdPartySignInUp(input) + user = response.user + const newUser = response.createdNewUser + if (newUser) { + throw { + error: 'signup error', + } + } + throw { + error: 'signin error', + } + }, + getUserById: async (input) => { + await oI.getUserById(input) + throw { + error: 'get user error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.get('/user', async (req, res, next) => { + try { + const userId = req.query.userId + res.json(await ThirdPartyPasswordless.getUserById(userId)) + } + catch (err) { + next(err) + } + }) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.deepStrictEqual(signUpResponse, { error: 'signup error', customError: true }) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + + const userByIdResponse = await new Promise(resolve => + request(app) + .get('/user') + .query({ + userId: user.id, + }) + .expect(200) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.deepStrictEqual(userByIdResponse, { error: 'get user error', customError: true }) + }) + + it('overriding api tests, throws error', async () => { + await startST() + let user + let newUser + const emailExists = undefined + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + providers: [customProvider1], + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: async (input) => { + const response = await oI.thirdPartySignInUpPOST(input) + user = response.user + newUser = response.createdNewUser + if (newUser) { + throw { + error: 'signup error', + } + } + throw { + error: 'signin error', + } + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').times(2).reply(200, {}) + + app.use((err, req, res, next) => { + res.json({ + ...err, + customError: true, + }) + }) + + const signUpResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.notStrictEqual(user, undefined) + assert.strictEqual(newUser, true) + assert.deepStrictEqual(signUpResponse, { error: 'signup error', customError: true }) + + const signInResponse = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(err.response.body) + + else + resolve(res.body) + }), + ) + + assert.strictEqual(newUser, false) + assert.deepStrictEqual(signInResponse, { error: 'signin error', customError: true }) + }) +}) diff --git a/test/thirdpartypasswordless/provider.test.js b/test/thirdpartypasswordless/provider.test.js deleted file mode 100644 index b2b034c6e..000000000 --- a/test/thirdpartypasswordless/provider.test.js +++ /dev/null @@ -1,814 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, isCDIVersionCompatible } = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyPasswordlessRecipe = require("../../lib/build/recipe/thirdpartypasswordless/recipe").default; -let ThirdPartyPasswordless = require("../../lib/build/recipe/thirdpartypasswordless"); -let { middleware, errorHandler } = require("../../framework/express"); - -const privateKey = `-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIP92u8DjfW31UDDudzWtcsiH/gJ5RpdgL6EV4FTuADZWoAcGBSuBBAAK\noUQDQgAEBorYK2YgYN1BDxVNtBgq8ZdoIR5m02kfJKFI/Vq1+uagvjjZVLpeUEgQ\n79ENddF5P8V8gRri+XzD2zNYpYXGNQ==\n-----END EC PRIVATE KEY-----`; - -/** - * TODO - * - Google - * - pass additional params, check they are present in authorisation url - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - * - Facebook - * - test minimum config - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - * - Github - * - test minimum config - * - pass additional params, check they are present in authorisation url - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - * - Apple - * - test minimum config - * - pass additional params, check they are present in authorisation url - * - pass additional/wrong config and check that error gets thrown - * - test passing scopes in config - */ -describe(`providerTest: ${printPath("[test/thirdpartypasswordless/provider.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test with thirdpartypasswordless, the minimum config for third party provider google", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test-secret", - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "google"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, "https://oauth2.googleapis.com/token"); - assert.strictEqual( - providerInfoGetResult.authorisationRedirect.url, - "https://accounts.google.com/o/oauth2/v2/auth" - ); - assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { - client_id: "test", - client_secret: "test-secret", - grant_type: "authorization_code", - }); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - scope: "https://www.googleapis.com/auth/userinfo.email", - }); - }); - - it("test with thirdPartyPasswordless, passing additional params, check they are present in authorisation url for thirdparty provider google", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test-secret", - authorisationRedirect: { - params: { - key1: "value1", - key2: "value2", - }, - }, - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "google"); - let providerInfoGetResult = await providerInfo.get(); - - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - scope: "https://www.googleapis.com/auth/userinfo.email", - key1: "value1", - key2: "value2", - }); - }); - - it("test for thirdPartyPasswordless, passing scopes in config for thirdparty provider google", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "google"); - let providerInfoGetResult = await providerInfo.get(); - - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - access_type: "offline", - include_granted_scopes: "true", - response_type: "code", - scope: "test-scope-1 test-scope-2", - }); - }); - - it("test for thirdPartyPasswordless, minimum config for third party provider facebook", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Facebook({ - clientId, - clientSecret, - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "facebook"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual( - providerInfoGetResult.accessTokenAPI.url, - "https://graph.facebook.com/v9.0/oauth/access_token" - ); - assert.strictEqual( - providerInfoGetResult.authorisationRedirect.url, - "https://www.facebook.com/v9.0/dialog/oauth" - ); - assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { - client_id: clientId, - client_secret: clientSecret, - }); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - response_type: "code", - scope: "email", - }); - }); - - it("test with thirdPartyPasswordless, passing scopes in config for third party provider facebook", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Facebook({ - clientId, - clientSecret, - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "facebook"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - response_type: "code", - scope: "test-scope-1 test-scope-2", - }); - }); - - it("test with thirdPartyPasswordless, minimum config for third party provider github", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordlessRecipe.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Github({ - clientId, - clientSecret, - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "github"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, "https://github.com/login/oauth/access_token"); - assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, "https://github.com/login/oauth/authorize"); - assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { - client_id: clientId, - client_secret: clientSecret, - }); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "read:user user:email", - }); - }); - - it("test with thirdPartyPasswordless, additional params, check they are present in authorisation url for third party provider github", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Github({ - clientId, - clientSecret, - authorisationRedirect: { - params: { - key1: "value1", - key2: "value2", - }, - }, - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "github"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "read:user user:email", - key1: "value1", - key2: "value2", - }); - }); - - it("test with thirdPartyPasswordless, passing scopes in config for third party provider github", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = "test-secret"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Github({ - clientId, - clientSecret, - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "github"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "test-scope-1 test-scope-2", - }); - }); - - it("test with thirdPartyPasswordless, minimum config for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey, - teamId: "test-team-id", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Apple({ - clientId, - clientSecret, - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "apple"); - let providerInfoGetResult = await providerInfo.get(); - assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, "https://appleid.apple.com/auth/token"); - assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, "https://appleid.apple.com/auth/authorize"); - - let accessTokenAPIParams = providerInfoGetResult.accessTokenAPI.params; - - assert(accessTokenAPIParams.client_id === clientId); - assert(accessTokenAPIParams.client_secret !== undefined); - assert(accessTokenAPIParams.grant_type === "authorization_code"); - - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "email", - response_mode: "form_post", - response_type: "code", - }); - }); - - it("test with thirdPartyPasswordless, passing additional params, check they are present in authorisation url for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey, - teamId: "test-team-id", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordlessRecipe.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Apple({ - clientId, - clientSecret, - authorisationRedirect: { - params: { - key1: "value1", - key2: "value2", - }, - }, - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "apple"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "email", - response_mode: "form_post", - response_type: "code", - key1: "value1", - key2: "value2", - }); - }); - - it("test with thirdPartyPasswordless, passing scopes in config for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey, - teamId: "test-team-id", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Apple({ - clientId, - clientSecret, - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0]; - assert.strictEqual(providerInfo.id, "apple"); - let providerInfoGetResult = await providerInfo.get(); - assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { - client_id: "test", - scope: "test-scope-1 test-scope-2", - response_mode: "form_post", - response_type: "code", - }); - }); - - it("test with thirdPartyPasswordless, passing invalid privateKey in config for third party provider apple", async function () { - await startST(); - - let clientId = "test"; - let clientSecret = { - keyId: "test-key", - privateKey: "invalidKey", - teamId: "test-team-id", - }; - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Apple({ - clientId, - clientSecret, - }), - ], - }), - ], - }); - - assert(false); - } catch (error) { - if (error.type !== ThirdPartyPasswordless.Error.BAD_INPUT_ERROR) { - throw error; - } - } - }); - - it("test with thirdPartyPasswordless duplicate provider without any default", async function () { - await startST(); - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }), - ], - }); - throw new Error("should fail"); - } catch (err) { - assert( - err.message === - `The providers array has multiple entries for the same third party provider. Please mark one of them as the default one by using "isDefault: true".` - ); - } - }); - - it("test with thirdPartyPasswordless, duplicate provider with both default", async function () { - await startST(); - try { - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Google({ - isDefault: true, - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ThirdPartyPasswordless.Google({ - isDefault: true, - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }), - ], - }); - throw new Error("should fail"); - } catch (err) { - assert( - err.message === - `You have provided multiple third party providers that have the id: "google" and are marked as "isDefault: true". Please only mark one of them as isDefault.` - ); - } - }); - it("test with thirdPartyPasswordless, duplicate provider with one default", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [ - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ThirdPartyPasswordless.Google({ - isDefault: true, - clientId: "test", - clientSecret: "test-secret", - scope: ["test-scope-1", "test-scope-2"], - }), - ], - }), - ], - }); - }); -}); diff --git a/test/thirdpartypasswordless/provider.test.ts b/test/thirdpartypasswordless/provider.test.ts new file mode 100644 index 000000000..56294e811 --- /dev/null +++ b/test/thirdpartypasswordless/provider.test.ts @@ -0,0 +1,806 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyPasswordlessRecipe from 'supertokens-node/recipe/thirdpartypasswordless/recipe' +import ThirdPartyPasswordless from 'supertokens-node/recipe/thirdpartypasswordless' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +const privateKey = '-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIP92u8DjfW31UDDudzWtcsiH/gJ5RpdgL6EV4FTuADZWoAcGBSuBBAAK\noUQDQgAEBorYK2YgYN1BDxVNtBgq8ZdoIR5m02kfJKFI/Vq1+uagvjjZVLpeUEgQ\n79ENddF5P8V8gRri+XzD2zNYpYXGNQ==\n-----END EC PRIVATE KEY-----' + +/** + * TODO + * - Google + * - pass additional params, check they are present in authorisation url + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + * - Facebook + * - test minimum config + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + * - Github + * - test minimum config + * - pass additional params, check they are present in authorisation url + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + * - Apple + * - test minimum config + * - pass additional params, check they are present in authorisation url + * - pass additional/wrong config and check that error gets thrown + * - test passing scopes in config + */ +describe(`providerTest: ${printPath('[test/thirdpartypasswordless/provider.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test with thirdpartypasswordless, the minimum config for third party provider google', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test-secret', + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'google') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, 'https://oauth2.googleapis.com/token') + assert.strictEqual( + providerInfoGetResult.authorisationRedirect.url, + 'https://accounts.google.com/o/oauth2/v2/auth', + ) + assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { + client_id: 'test', + client_secret: 'test-secret', + grant_type: 'authorization_code', + }) + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + scope: 'https://www.googleapis.com/auth/userinfo.email', + }) + }) + + it('test with thirdPartyPasswordless, passing additional params, check they are present in authorisation url for thirdparty provider google', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test-secret', + authorisationRedirect: { + params: { + key1: 'value1', + key2: 'value2', + }, + }, + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'google') + const providerInfoGetResult = await providerInfo.get() + + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + scope: 'https://www.googleapis.com/auth/userinfo.email', + key1: 'value1', + key2: 'value2', + }) + }) + + it('test for thirdPartyPasswordless, passing scopes in config for thirdparty provider google', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'google') + const providerInfoGetResult = await providerInfo.get() + + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + access_type: 'offline', + include_granted_scopes: 'true', + response_type: 'code', + scope: 'test-scope-1 test-scope-2', + }) + }) + + it('test for thirdPartyPasswordless, minimum config for third party provider facebook', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Facebook({ + clientId, + clientSecret, + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'facebook') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual( + providerInfoGetResult.accessTokenAPI.url, + 'https://graph.facebook.com/v9.0/oauth/access_token', + ) + assert.strictEqual( + providerInfoGetResult.authorisationRedirect.url, + 'https://www.facebook.com/v9.0/dialog/oauth', + ) + assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { + client_id: clientId, + client_secret: clientSecret, + }) + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + response_type: 'code', + scope: 'email', + }) + }) + + it('test with thirdPartyPasswordless, passing scopes in config for third party provider facebook', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Facebook({ + clientId, + clientSecret, + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'facebook') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + response_type: 'code', + scope: 'test-scope-1 test-scope-2', + }) + }) + + it('test with thirdPartyPasswordless, minimum config for third party provider github', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordlessRecipe.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Github({ + clientId, + clientSecret, + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'github') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, 'https://github.com/login/oauth/access_token') + assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, 'https://github.com/login/oauth/authorize') + assert.deepStrictEqual(providerInfoGetResult.accessTokenAPI.params, { + client_id: clientId, + client_secret: clientSecret, + }) + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'read:user user:email', + }) + }) + + it('test with thirdPartyPasswordless, additional params, check they are present in authorisation url for third party provider github', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Github({ + clientId, + clientSecret, + authorisationRedirect: { + params: { + key1: 'value1', + key2: 'value2', + }, + }, + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'github') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'read:user user:email', + key1: 'value1', + key2: 'value2', + }) + }) + + it('test with thirdPartyPasswordless, passing scopes in config for third party provider github', async () => { + await startST() + + const clientId = 'test' + const clientSecret = 'test-secret' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Github({ + clientId, + clientSecret, + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'github') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'test-scope-1 test-scope-2', + }) + }) + + it('test with thirdPartyPasswordless, minimum config for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey, + teamId: 'test-team-id', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Apple({ + clientId, + clientSecret, + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'apple') + const providerInfoGetResult = await providerInfo.get() + assert.strictEqual(providerInfoGetResult.accessTokenAPI.url, 'https://appleid.apple.com/auth/token') + assert.strictEqual(providerInfoGetResult.authorisationRedirect.url, 'https://appleid.apple.com/auth/authorize') + + const accessTokenAPIParams = providerInfoGetResult.accessTokenAPI.params + + assert(accessTokenAPIParams.client_id === clientId) + assert(accessTokenAPIParams.client_secret !== undefined) + assert(accessTokenAPIParams.grant_type === 'authorization_code') + + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'email', + response_mode: 'form_post', + response_type: 'code', + }) + }) + + it('test with thirdPartyPasswordless, passing additional params, check they are present in authorisation url for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey, + teamId: 'test-team-id', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordlessRecipe.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Apple({ + clientId, + clientSecret, + authorisationRedirect: { + params: { + key1: 'value1', + key2: 'value2', + }, + }, + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'apple') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'email', + response_mode: 'form_post', + response_type: 'code', + key1: 'value1', + key2: 'value2', + }) + }) + + it('test with thirdPartyPasswordless, passing scopes in config for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey, + teamId: 'test-team-id', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Apple({ + clientId, + clientSecret, + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const providerInfo = ThirdPartyPasswordlessRecipe.getInstanceOrThrowError().config.providers[0] + assert.strictEqual(providerInfo.id, 'apple') + const providerInfoGetResult = await providerInfo.get() + assert.deepStrictEqual(providerInfoGetResult.authorisationRedirect.params, { + client_id: 'test', + scope: 'test-scope-1 test-scope-2', + response_mode: 'form_post', + response_type: 'code', + }) + }) + + it('test with thirdPartyPasswordless, passing invalid privateKey in config for third party provider apple', async () => { + await startST() + + const clientId = 'test' + const clientSecret = { + keyId: 'test-key', + privateKey: 'invalidKey', + teamId: 'test-team-id', + } + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Apple({ + clientId, + clientSecret, + }), + ], + }), + ], + }) + + assert(false) + } + catch (error) { + if (error.type !== ThirdPartyPasswordless.Error.BAD_INPUT_ERROR) + throw error + } + }) + + it('test with thirdPartyPasswordless duplicate provider without any default', async () => { + await startST() + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }), + ], + }) + throw new Error('should fail') + } + catch (err) { + assert( + err.message + === 'The providers array has multiple entries for the same third party provider. Please mark one of them as the default one by using "isDefault: true".', + ) + } + }) + + it('test with thirdPartyPasswordless, duplicate provider with both default', async () => { + await startST() + try { + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Google({ + isDefault: true, + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ThirdPartyPasswordless.Google({ + isDefault: true, + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }), + ], + }) + throw new Error('should fail') + } + catch (err) { + assert( + err.message + === 'You have provided multiple third party providers that have the id: "google" and are marked as "isDefault: true". Please only mark one of them as isDefault.', + ) + } + }) + it('test with thirdPartyPasswordless, duplicate provider with one default', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [ + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ThirdPartyPasswordless.Google({ + isDefault: true, + clientId: 'test', + clientSecret: 'test-secret', + scope: ['test-scope-1', 'test-scope-2'], + }), + ], + }), + ], + }) + }) +}) diff --git a/test/thirdpartypasswordless/recipeFunctions.test.js b/test/thirdpartypasswordless/recipeFunctions.test.js deleted file mode 100644 index b71117b14..000000000 --- a/test/thirdpartypasswordless/recipeFunctions.test.js +++ /dev/null @@ -1,1063 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, setKeyValueInConfig } = require("../utils"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let ThirdPartyPasswordless = require("../../recipe/thirdpartypasswordless"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -const EmailVerification = require("../../recipe/emailverification"); -let { isCDIVersionCompatible } = require("../utils"); - -describe(`recipeFunctions: ${printPath("[test/thirdpartypasswordless/recipeFunctions.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - // test that creating a user with ThirdParty, and they have a verified email that, isEmailVerified returns true and the opposite case - it("test with thirdPartyPasswordless, for ThirdParty user that isEmailVerified returns the correct email verification status", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [/** @type {any} */ {}], - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - // create a ThirdParty user with a verified email - let response = await ThirdPartyPasswordless.thirdPartySignInUp( - "customProvider", - "verifiedUser", - "test@example.com" - ); - - // verify the user's email - let emailVerificationToken = await EmailVerification.createEmailVerificationToken(response.user.id); - await EmailVerification.verifyEmailUsingToken(emailVerificationToken.token); - - // check that the ThirdParty user's email is verified - assert(await EmailVerification.isEmailVerified(response.user.id)); - - // create a ThirdParty user with an unverfied email and check that it is not verified - let response2 = await ThirdPartyPasswordless.thirdPartySignInUp( - "customProvider2", - "NotVerifiedUser", - "test@example.com" - ); - - assert(!(await EmailVerification.isEmailVerified(response2.user.id))); - }); - - it("test with thirdPartyPasswordless, for Passwordless user that isEmailVerified returns true for both email and phone", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - EmailVerification.init({ mode: "OPTIONAL" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - // create a Passwordless user with email - let response = await ThirdPartyPasswordless.passwordlessSignInUp({ - email: "test@example.com", - }); - - // verify the user's email - let emailVerificationToken = await EmailVerification.createEmailVerificationToken(response.user.id); - await EmailVerification.verifyEmailUsingToken(emailVerificationToken.token); - - // check that the Passwordless user's email is verified - assert(await EmailVerification.isEmailVerified(response.user.id)); - - // check that creating an email verification with a verified passwordless user should return EMAIL_ALREADY_VERIFIED_ERROR - assert( - (await EmailVerification.createEmailVerificationToken(response.user.id)).status === - "EMAIL_ALREADY_VERIFIED_ERROR" - ); - - // create a Passwordless user with phone and check that it is verified - let response2 = await ThirdPartyPasswordless.passwordlessSignInUp({ - phoneNumber: "+123456789012", - }); - - // check that the Passwordless phone number user's is automatically verified - assert(await EmailVerification.isEmailVerified(response2.user.id)); - - // check that creating an email verification with a phone-based passwordless user should return EMAIL_ALREADY_VERIFIED_ERROR - assert.equal( - (await EmailVerification.createEmailVerificationToken(response2.user.id)).status, - "EMAIL_ALREADY_VERIFIED_ERROR" - ); - }); - - it("test with thirdPartyPasswordless, getUser functionality", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let user = await ThirdPartyPasswordless.getUserById({ - userId: "random", - }); - - assert(user === undefined); - - user = ( - await ThirdPartyPasswordless.passwordlessSignInUp({ - email: "test@example.com", - }) - ).user; - - let userId = user.id; - let result = await ThirdPartyPasswordless.getUserById(userId); - - assert(result.id === user.id); - assert(result.email !== undefined && user.email === result.email); - assert(result.phoneNumber === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 3); - } - - { - let users = await ThirdPartyPasswordless.getUsersByEmail({ - email: "random", - }); - - assert(users.length === 0); - - let user = ( - await ThirdPartyPasswordless.passwordlessSignInUp({ - email: "test@example.com", - }) - ).user; - - let result = await ThirdPartyPasswordless.getUsersByEmail(user.email); - - assert(result.length === 1); - - let userInfo = result[0]; - - assert(userInfo.id === user.id); - assert(userInfo.email === user.email); - assert(userInfo.phoneNumber === undefined); - assert(typeof userInfo.timeJoined === "number"); - assert(Object.keys(userInfo).length === 3); - } - - { - let user = await ThirdPartyPasswordless.getUserByPhoneNumber({ - phoneNumber: "random", - }); - - assert(user === undefined); - - user = ( - await ThirdPartyPasswordless.passwordlessSignInUp({ - phoneNumber: "+1234567890", - }) - ).user; - - let result = await ThirdPartyPasswordless.getUserByPhoneNumber({ - phoneNumber: user.phoneNumber, - }); - assert(result.id === user.id); - assert(result.phoneNumber !== undefined && user.phoneNumber === result.phoneNumber); - assert(result.email === undefined); - assert(typeof result.timeJoined === "number"); - assert(Object.keys(result).length === 3); - } - }); - - it("test with thirdPartyPasswordless, createCode test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let resp = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - - { - let resp = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - userInputCode: "123", - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - }); - - it("thirdPartyPasswordless createNewCodeForDevice test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let resp = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - resp = await ThirdPartyPasswordless.createNewCodeForDevice({ - deviceId: resp.deviceId, - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - - { - let resp = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - resp = await ThirdPartyPasswordless.createNewCodeForDevice({ - deviceId: resp.deviceId, - userInputCode: "1234", - }); - - assert(resp.status === "OK"); - assert(typeof resp.preAuthSessionId === "string"); - assert(typeof resp.codeId === "string"); - assert(typeof resp.deviceId === "string"); - assert(typeof resp.userInputCode === "string"); - assert(typeof resp.linkCode === "string"); - assert(typeof resp.codeLifetime === "number"); - assert(typeof resp.timeCreated === "number"); - assert(Object.keys(resp).length === 8); - } - - { - let resp = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - resp = await ThirdPartyPasswordless.createNewCodeForDevice({ - deviceId: "random", - }); - - assert(resp.status === "RESTART_FLOW_ERROR"); - assert(Object.keys(resp).length === 1); - } - - { - let resp = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - userInputCode: "1234", - }); - - resp = await ThirdPartyPasswordless.createNewCodeForDevice({ - deviceId: resp.deviceId, - userInputCode: "1234", - }); - - assert(resp.status === "USER_INPUT_CODE_ALREADY_USED_ERROR"); - assert(Object.keys(resp).length === 1); - } - }); - - it("thirdPartyPasswordless consumeCode test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - let resp = await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert(resp.status === "OK"); - assert(resp.createdNewUser); - assert(typeof resp.user.id === "string"); - assert(resp.user.email === "test@example.com"); - assert(resp.user.phoneNumber === undefined); - assert(typeof resp.user.timeJoined === "number"); - assert(Object.keys(resp).length === 3); - assert(Object.keys(resp.user).length === 3); - } - - { - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - let resp = await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: "random", - deviceId: codeInfo.deviceId, - }); - - assert(resp.status === "INCORRECT_USER_INPUT_CODE_ERROR"); - assert(resp.failedCodeInputAttemptCount === 1); - assert(resp.maximumCodeInputAttempts === 5); - assert(Object.keys(resp).length === 3); - } - - { - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - try { - await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: "random", - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - assert(false); - } catch (err) { - assert(err.message.includes("preAuthSessionId and deviceId doesn't match")); - } - } - }); - - it("thirdPartyPasswordless, consumeCode test with EXPIRED_USER_INPUT_CODE_ERROR", async function () { - await setKeyValueInConfig("passwordless_code_lifetime", 1000); // one second lifetime - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - { - let codeInfo = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - await new Promise((r) => setTimeout(r, 2000)); // wait for code to expire - - let resp = await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert(resp.status === "EXPIRED_USER_INPUT_CODE_ERROR"); - assert(resp.failedCodeInputAttemptCount === 1); - assert(resp.maximumCodeInputAttempts === 5); - assert(Object.keys(resp).length === 3); - } - }); - - // updateUser - it("thirdPartyPasswordless, updateUser contactMethod email test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let userInfo = await ThirdPartyPasswordless.passwordlessSignInUp({ - email: "test@example.com", - }); - { - // update users email - let response = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo.user.id, - email: "test2@example.com", - }); - assert(response.status === "OK"); - - let result = await ThirdPartyPasswordless.getUserById(userInfo.user.id); - - assert(result.email === "test2@example.com"); - } - { - // update user with invalid userId - let response = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: "invalidUserId", - email: "test2@example.com", - }); - - assert(response.status === "UNKNOWN_USER_ID_ERROR"); - } - { - // update user with an email that already exists - let userInfo2 = await ThirdPartyPasswordless.passwordlessSignInUp({ - email: "test3@example.com", - }); - - let result = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo2.user.id, - email: "test2@example.com", - }); - - assert(result.status === "EMAIL_ALREADY_EXISTS_ERROR"); - } - }); - - it("thirdPartyPasswordless, updateUser contactMethod phone test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let phoneNumber_1 = "+1234567891"; - let phoneNumber_2 = "+1234567892"; - let phoneNumber_3 = "+1234567893"; - - let userInfo = await ThirdPartyPasswordless.passwordlessSignInUp({ - phoneNumber: phoneNumber_1, - }); - - { - // update users email - let response = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo.user.id, - phoneNumber: phoneNumber_2, - }); - assert(response.status === "OK"); - - let result = await ThirdPartyPasswordless.getUserById(userInfo.user.id); - - assert(result.phoneNumber === phoneNumber_2); - } - { - // update user with a phoneNumber that already exists - let userInfo2 = await ThirdPartyPasswordless.passwordlessSignInUp({ - phoneNumber: phoneNumber_3, - }); - - let result = await ThirdPartyPasswordless.updatePasswordlessUser({ - userId: userInfo2.user.id, - phoneNumber: phoneNumber_2, - }); - - assert(result.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR"); - } - }); - - // revokeAllCodes - it("thirdPartyPasswordless, revokeAllCodes test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - let codeInfo_2 = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - { - let result = await ThirdPartyPasswordless.revokeAllCodes({ - email: "test@example.com", - }); - - assert(result.status === "OK"); - } - - { - let result_1 = await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo_1.preAuthSessionId, - deviceId: codeInfo_1.deviceId, - userInputCode: codeInfo_1.userInputCode, - }); - - assert(result_1.status === "RESTART_FLOW_ERROR"); - - let result_2 = await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo_2.preAuthSessionId, - deviceId: codeInfo_2.deviceId, - userInputCode: codeInfo_2.userInputCode, - }); - - assert(result_2.status === "RESTART_FLOW_ERROR"); - } - }); - - it("thirdPartyPasswordless, revokeCode test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - let codeInfo_2 = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - { - let result = await ThirdPartyPasswordless.revokeCode({ - codeId: codeInfo_1.codeId, - }); - - assert(result.status === "OK"); - } - - { - let result_1 = await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo_1.preAuthSessionId, - deviceId: codeInfo_1.deviceId, - userInputCode: codeInfo_1.userInputCode, - }); - - assert(result_1.status === "RESTART_FLOW_ERROR"); - - let result_2 = await ThirdPartyPasswordless.consumeCode({ - preAuthSessionId: codeInfo_2.preAuthSessionId, - deviceId: codeInfo_2.deviceId, - userInputCode: codeInfo_2.userInputCode, - }); - - assert(result_2.status === "OK"); - } - }); - - // listCodesByEmail - it("thirdPartyPasswordless, listCodesByEmail test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - let codeInfo_2 = await ThirdPartyPasswordless.createCode({ - email: "test@example.com", - }); - - let result = await ThirdPartyPasswordless.listCodesByEmail({ - email: "test@example.com", - }); - assert(result.length === 2); - result.forEach((element) => { - element.codes.forEach((code) => { - if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) { - assert(false); - } - }); - }); - }); - - //listCodesByPhoneNumber - it("thirdPartyPasswordless, listCodesByPhoneNumber test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await ThirdPartyPasswordless.createCode({ - phoneNumber: "+1234567890", - }); - - let codeInfo_2 = await ThirdPartyPasswordless.createCode({ - phoneNumber: "+1234567890", - }); - - let result = await ThirdPartyPasswordless.listCodesByPhoneNumber({ - phoneNumber: "+1234567890", - }); - assert(result.length === 2); - result.forEach((element) => { - element.codes.forEach((code) => { - if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) { - assert(false); - } - }); - }); - }); - - // listCodesByDeviceId and listCodesByPreAuthSessionId - it("thirdPartyPasswordless, listCodesByDeviceId and listCodesByPreAuthSessionId test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let codeInfo_1 = await ThirdPartyPasswordless.createCode({ - phoneNumber: "+1234567890", - }); - - { - let result = await ThirdPartyPasswordless.listCodesByDeviceId({ - deviceId: codeInfo_1.deviceId, - }); - assert(result.codes[0].codeId === codeInfo_1.codeId); - } - - { - let result = await ThirdPartyPasswordless.listCodesByPreAuthSessionId({ - preAuthSessionId: codeInfo_1.preAuthSessionId, - }); - assert(result.codes[0].codeId === codeInfo_1.codeId); - } - }); - - /* - - createMagicLink - - check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode - */ - - it("thirdPartyPasswordless, createMagicLink test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let result = await ThirdPartyPasswordless.createMagicLink({ - phoneNumber: "+1234567890", - }); - - let magicLinkURL = new URL(result); - - assert(magicLinkURL.hostname === "supertokens.io"); - assert(magicLinkURL.pathname === "/auth/verify"); - assert(magicLinkURL.searchParams.get("rid") === "thirdpartypasswordless"); - assert(typeof magicLinkURL.searchParams.get("preAuthSessionId") === "string"); - assert(magicLinkURL.hash.length > 1); - }); - - // signInUp test - it("thirdPartyPasswordless, signInUp test", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let result = await ThirdPartyPasswordless.passwordlessSignInUp({ - phoneNumber: "+12345678901", - }); - - assert(result.status === "OK"); - assert(result.createdNewUser === true); - assert(Object.keys(result).length === 3); - - assert(result.user.phoneNumber === "+12345678901"); - assert(typeof result.user.id === "string"); - assert(typeof result.user.timeJoined === "number"); - assert(Object.keys(result.user).length === 3); - }); -}); diff --git a/test/thirdpartypasswordless/recipeFunctions.test.ts b/test/thirdpartypasswordless/recipeFunctions.test.ts new file mode 100644 index 000000000..4b6756fe0 --- /dev/null +++ b/test/thirdpartypasswordless/recipeFunctions.test.ts @@ -0,0 +1,1041 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import ThirdPartyPasswordless from 'supertokens-node/recipe/thirdpartypasswordless' +import { ProcessState } from 'supertokens-node/processState' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, isCDIVersionCompatible, killAllST, printPath, setKeyValueInConfig, setupST, startST } from '../utils' + +describe(`recipeFunctions: ${printPath('[test/thirdpartypasswordless/recipeFunctions.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + // test that creating a user with ThirdParty, and they have a verified email that, isEmailVerified returns true and the opposite case + it('test with thirdPartyPasswordless, for ThirdParty user that isEmailVerified returns the correct email verification status', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [{}], + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + // create a ThirdParty user with a verified email + const response = await ThirdPartyPasswordless.thirdPartySignInUp( + 'customProvider', + 'verifiedUser', + 'test@example.com', + ) + + // verify the user's email + const emailVerificationToken = await EmailVerification.createEmailVerificationToken(response.user.id) + await EmailVerification.verifyEmailUsingToken(emailVerificationToken.token) + + // check that the ThirdParty user's email is verified + assert(await EmailVerification.isEmailVerified(response.user.id)) + + // create a ThirdParty user with an unverfied email and check that it is not verified + const response2 = await ThirdPartyPasswordless.thirdPartySignInUp( + 'customProvider2', + 'NotVerifiedUser', + 'test@example.com', + ) + + assert(!(await EmailVerification.isEmailVerified(response2.user.id))) + }) + + it('test with thirdPartyPasswordless, for Passwordless user that isEmailVerified returns true for both email and phone', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + EmailVerification.init({ mode: 'OPTIONAL' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + // create a Passwordless user with email + const response = await ThirdPartyPasswordless.passwordlessSignInUp({ + email: 'test@example.com', + }) + + // verify the user's email + const emailVerificationToken = await EmailVerification.createEmailVerificationToken(response.user.id) + await EmailVerification.verifyEmailUsingToken(emailVerificationToken.token) + + // check that the Passwordless user's email is verified + assert(await EmailVerification.isEmailVerified(response.user.id)) + + // check that creating an email verification with a verified passwordless user should return EMAIL_ALREADY_VERIFIED_ERROR + assert( + (await EmailVerification.createEmailVerificationToken(response.user.id)).status + === 'EMAIL_ALREADY_VERIFIED_ERROR', + ) + + // create a Passwordless user with phone and check that it is verified + const response2 = await ThirdPartyPasswordless.passwordlessSignInUp({ + phoneNumber: '+123456789012', + }) + + // check that the Passwordless phone number user's is automatically verified + assert(await EmailVerification.isEmailVerified(response2.user.id)) + + // check that creating an email verification with a phone-based passwordless user should return EMAIL_ALREADY_VERIFIED_ERROR + assert.equal( + (await EmailVerification.createEmailVerificationToken(response2.user.id)).status, + 'EMAIL_ALREADY_VERIFIED_ERROR', + ) + }) + + it('test with thirdPartyPasswordless, getUser functionality', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + const user = ( + await ThirdPartyPasswordless.passwordlessSignInUp({ + email: 'test@example.com', + }) + ).user + + const userId = user.id + const result = await ThirdPartyPasswordless.getUserById(userId) + + assert(result.id === user.id) + assert(result.email !== undefined && user.email === result.email) + assert(result.phoneNumber === undefined) + assert(typeof result.timeJoined === 'number') + assert(Object.keys(result).length === 3) + } + + { + const users = await ThirdPartyPasswordless.getUsersByEmail({ + email: 'random', + }) + + assert(users.length === 0) + + const user = ( + await ThirdPartyPasswordless.passwordlessSignInUp({ + email: 'test@example.com', + }) + ).user + + const result = await ThirdPartyPasswordless.getUsersByEmail(user.email) + + assert(result.length === 1) + + const userInfo = result[0] + + assert(userInfo.id === user.id) + assert(userInfo.email === user.email) + assert(userInfo.phoneNumber === undefined) + assert(typeof userInfo.timeJoined === 'number') + assert(Object.keys(userInfo).length === 3) + } + + { + let user = await ThirdPartyPasswordless.getUserByPhoneNumber({ + phoneNumber: 'random', + }) + + assert(user === undefined) + + user = ( + await ThirdPartyPasswordless.passwordlessSignInUp({ + phoneNumber: '+1234567890', + }) + ).user + + const result = await ThirdPartyPasswordless.getUserByPhoneNumber({ + phoneNumber: user.phoneNumber, + }) + assert(result.id === user.id) + assert(result.phoneNumber !== undefined && user.phoneNumber === result.phoneNumber) + assert(result.email === undefined) + assert(typeof result.timeJoined === 'number') + assert(Object.keys(result).length === 3) + } + }) + + it('test with thirdPartyPasswordless, createCode test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + const resp = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + + { + const resp = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + userInputCode: '123', + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + }) + + it('thirdPartyPasswordless createNewCodeForDevice test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + let resp = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + resp = await ThirdPartyPasswordless.createNewCodeForDevice({ + deviceId: resp.deviceId, + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + + { + let resp = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + resp = await ThirdPartyPasswordless.createNewCodeForDevice({ + deviceId: resp.deviceId, + userInputCode: '1234', + }) + + assert(resp.status === 'OK') + assert(typeof resp.preAuthSessionId === 'string') + assert(typeof resp.codeId === 'string') + assert(typeof resp.deviceId === 'string') + assert(typeof resp.userInputCode === 'string') + assert(typeof resp.linkCode === 'string') + assert(typeof resp.codeLifetime === 'number') + assert(typeof resp.timeCreated === 'number') + assert(Object.keys(resp).length === 8) + } + + { + let resp = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + resp = await ThirdPartyPasswordless.createNewCodeForDevice({ + deviceId: 'random', + }) + + assert(resp.status === 'RESTART_FLOW_ERROR') + assert(Object.keys(resp).length === 1) + } + + { + let resp = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + userInputCode: '1234', + }) + + resp = await ThirdPartyPasswordless.createNewCodeForDevice({ + deviceId: resp.deviceId, + userInputCode: '1234', + }) + + assert(resp.status === 'USER_INPUT_CODE_ALREADY_USED_ERROR') + assert(Object.keys(resp).length === 1) + } + }) + + it('thirdPartyPasswordless consumeCode test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + const resp = await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert(resp.status === 'OK') + assert(resp.createdNewUser) + assert(typeof resp.user.id === 'string') + assert(resp.user.email === 'test@example.com') + assert(resp.user.phoneNumber === undefined) + assert(typeof resp.user.timeJoined === 'number') + assert(Object.keys(resp).length === 3) + assert(Object.keys(resp.user).length === 3) + } + + { + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + const resp = await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: 'random', + deviceId: codeInfo.deviceId, + }) + + assert(resp.status === 'INCORRECT_USER_INPUT_CODE_ERROR') + assert(resp.failedCodeInputAttemptCount === 1) + assert(resp.maximumCodeInputAttempts === 5) + assert(Object.keys(resp).length === 3) + } + + { + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + try { + await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: 'random', + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + assert(false) + } + catch (err) { + assert(err.message.includes('preAuthSessionId and deviceId doesn\'t match')) + } + } + }) + + it('thirdPartyPasswordless, consumeCode test with EXPIRED_USER_INPUT_CODE_ERROR', async () => { + await setKeyValueInConfig('passwordless_code_lifetime', 1000) // one second lifetime + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + { + const codeInfo = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + await new Promise(r => setTimeout(r, 2000)) // wait for code to expire + + const resp = await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert(resp.status === 'EXPIRED_USER_INPUT_CODE_ERROR') + assert(resp.failedCodeInputAttemptCount === 1) + assert(resp.maximumCodeInputAttempts === 5) + assert(Object.keys(resp).length === 3) + } + }) + + // updateUser + it('thirdPartyPasswordless, updateUser contactMethod email test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const userInfo = await ThirdPartyPasswordless.passwordlessSignInUp({ + email: 'test@example.com', + }) + { + // update users email + const response = await ThirdPartyPasswordless.updatePasswordlessUser({ + userId: userInfo.user.id, + email: 'test2@example.com', + }) + assert(response.status === 'OK') + + const result = await ThirdPartyPasswordless.getUserById(userInfo.user.id) + + assert(result.email === 'test2@example.com') + } + { + // update user with invalid userId + const response = await ThirdPartyPasswordless.updatePasswordlessUser({ + userId: 'invalidUserId', + email: 'test2@example.com', + }) + + assert(response.status === 'UNKNOWN_USER_ID_ERROR') + } + { + // update user with an email that already exists + const userInfo2 = await ThirdPartyPasswordless.passwordlessSignInUp({ + email: 'test3@example.com', + }) + + const result = await ThirdPartyPasswordless.updatePasswordlessUser({ + userId: userInfo2.user.id, + email: 'test2@example.com', + }) + + assert(result.status === 'EMAIL_ALREADY_EXISTS_ERROR') + } + }) + + it('thirdPartyPasswordless, updateUser contactMethod phone test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const phoneNumber_1 = '+1234567891' + const phoneNumber_2 = '+1234567892' + const phoneNumber_3 = '+1234567893' + + const userInfo = await ThirdPartyPasswordless.passwordlessSignInUp({ + phoneNumber: phoneNumber_1, + }) + + { + // update users email + const response = await ThirdPartyPasswordless.updatePasswordlessUser({ + userId: userInfo.user.id, + phoneNumber: phoneNumber_2, + }) + assert(response.status === 'OK') + + const result = await ThirdPartyPasswordless.getUserById(userInfo.user.id) + + assert(result.phoneNumber === phoneNumber_2) + } + { + // update user with a phoneNumber that already exists + const userInfo2 = await ThirdPartyPasswordless.passwordlessSignInUp({ + phoneNumber: phoneNumber_3, + }) + + const result = await ThirdPartyPasswordless.updatePasswordlessUser({ + userId: userInfo2.user.id, + phoneNumber: phoneNumber_2, + }) + + assert(result.status === 'PHONE_NUMBER_ALREADY_EXISTS_ERROR') + } + }) + + // revokeAllCodes + it('thirdPartyPasswordless, revokeAllCodes test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + const codeInfo_2 = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + { + const result = await ThirdPartyPasswordless.revokeAllCodes({ + email: 'test@example.com', + }) + + assert(result.status === 'OK') + } + + { + const result_1 = await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo_1.preAuthSessionId, + deviceId: codeInfo_1.deviceId, + userInputCode: codeInfo_1.userInputCode, + }) + + assert(result_1.status === 'RESTART_FLOW_ERROR') + + const result_2 = await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo_2.preAuthSessionId, + deviceId: codeInfo_2.deviceId, + userInputCode: codeInfo_2.userInputCode, + }) + + assert(result_2.status === 'RESTART_FLOW_ERROR') + } + }) + + it('thirdPartyPasswordless, revokeCode test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + const codeInfo_2 = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + { + const result = await ThirdPartyPasswordless.revokeCode({ + codeId: codeInfo_1.codeId, + }) + + assert(result.status === 'OK') + } + + { + const result_1 = await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo_1.preAuthSessionId, + deviceId: codeInfo_1.deviceId, + userInputCode: codeInfo_1.userInputCode, + }) + + assert(result_1.status === 'RESTART_FLOW_ERROR') + + const result_2 = await ThirdPartyPasswordless.consumeCode({ + preAuthSessionId: codeInfo_2.preAuthSessionId, + deviceId: codeInfo_2.deviceId, + userInputCode: codeInfo_2.userInputCode, + }) + + assert(result_2.status === 'OK') + } + }) + + // listCodesByEmail + it('thirdPartyPasswordless, listCodesByEmail test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + const codeInfo_2 = await ThirdPartyPasswordless.createCode({ + email: 'test@example.com', + }) + + const result = await ThirdPartyPasswordless.listCodesByEmail({ + email: 'test@example.com', + }) + assert(result.length === 2) + result.forEach((element) => { + element.codes.forEach((code) => { + if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) + assert(false) + }) + }) + }) + + // listCodesByPhoneNumber + it('thirdPartyPasswordless, listCodesByPhoneNumber test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await ThirdPartyPasswordless.createCode({ + phoneNumber: '+1234567890', + }) + + const codeInfo_2 = await ThirdPartyPasswordless.createCode({ + phoneNumber: '+1234567890', + }) + + const result = await ThirdPartyPasswordless.listCodesByPhoneNumber({ + phoneNumber: '+1234567890', + }) + assert(result.length === 2) + result.forEach((element) => { + element.codes.forEach((code) => { + if (!(code.codeId === codeInfo_1.codeId || code.codeId === codeInfo_2.codeId)) + assert(false) + }) + }) + }) + + // listCodesByDeviceId and listCodesByPreAuthSessionId + it('thirdPartyPasswordless, listCodesByDeviceId and listCodesByPreAuthSessionId test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const codeInfo_1 = await ThirdPartyPasswordless.createCode({ + phoneNumber: '+1234567890', + }) + + { + const result = await ThirdPartyPasswordless.listCodesByDeviceId({ + deviceId: codeInfo_1.deviceId, + }) + assert(result.codes[0].codeId === codeInfo_1.codeId) + } + + { + const result = await ThirdPartyPasswordless.listCodesByPreAuthSessionId({ + preAuthSessionId: codeInfo_1.preAuthSessionId, + }) + assert(result.codes[0].codeId === codeInfo_1.codeId) + } + }) + + /* + - createMagicLink + - check that the magicLink format is {websiteDomain}{websiteBasePath}/verify?rid=passwordless&preAuthSessionId=#linkCode + */ + + it('thirdPartyPasswordless, createMagicLink test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const result = await ThirdPartyPasswordless.createMagicLink({ + phoneNumber: '+1234567890', + }) + + const magicLinkURL = new URL(result) + + assert(magicLinkURL.hostname === 'supertokens.io') + assert(magicLinkURL.pathname === '/auth/verify') + assert(magicLinkURL.searchParams.get('rid') === 'thirdpartypasswordless') + assert(typeof magicLinkURL.searchParams.get('preAuthSessionId') === 'string') + assert(magicLinkURL.hash.length > 1) + }) + + // signInUp test + it('thirdPartyPasswordless, signInUp test', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: (input) => { + + }, + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const result = await ThirdPartyPasswordless.passwordlessSignInUp({ + phoneNumber: '+12345678901', + }) + + assert(result.status === 'OK') + assert(result.createdNewUser === true) + assert(Object.keys(result).length === 3) + + assert(result.user.phoneNumber === '+12345678901') + assert(typeof result.user.id === 'string') + assert(typeof result.user.timeJoined === 'number') + assert(Object.keys(result.user).length === 3) + }) +}) diff --git a/test/thirdpartypasswordless/signinupFeature.test.js b/test/thirdpartypasswordless/signinupFeature.test.js deleted file mode 100644 index 48bde4760..000000000 --- a/test/thirdpartypasswordless/signinupFeature.test.js +++ /dev/null @@ -1,1146 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - isCDIVersionCompatible, -} = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyPasswordless = require("../../lib/build/recipe/thirdpartypasswordless"); -const EmailVerification = require("../../lib/build/recipe/emailverification"); -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signinupTest: ${printPath("[test/thirdpartypasswordless/signinupFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider2 = { - id: "custom", - get: (recipe, authCode) => { - throw new Error("error from get function"); - }, - }; - this.customProvider3 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider4 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - throw new Error("error from getProfileInfo"); - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider5 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: false, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - this.customProvider6 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - if (authCodeResponse.access_token === undefined) { - return {}; - } - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test with thirdPartyPasswordless, that if you disable the signInUp api, it does not work", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: undefined, - }; - }, - }, - providers: [ - ThirdPartyPasswordless.Google({ - clientId: "test", - clientSecret: "test-secret", - }), - ], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "google", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.status, 404); - }); - - it("test with thirdPartyPasswordless, minimum config without code for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL", createAndSendCustomEmail: () => {} }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider6], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - authCodeResponse: { - access_token: "saodiasjodai", - }, - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); - - let cookies1 = extractInfoFromResponse(response1); - assert.notStrictEqual(cookies1.accessToken, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.notStrictEqual(cookies1.antiCsrf, undefined); - assert.notStrictEqual(cookies1.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.strictEqual(cookies1.accessTokenDomain, undefined); - assert.strictEqual(cookies1.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies1.frontToken, "remove"); - - assert.strictEqual( - await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), - true - ); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - authCodeResponse: { - access_token: "saodiasjodai", - }, - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.notStrictEqual(response2, undefined); - assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); - - let cookies2 = extractInfoFromResponse(response2); - assert.notStrictEqual(cookies2.accessToken, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.notStrictEqual(cookies2.antiCsrf, undefined); - assert.notStrictEqual(cookies2.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.strictEqual(cookies2.accessTokenDomain, undefined); - assert.strictEqual(cookies2.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies2.frontToken, "remove"); - }); - - it("test with thirdPartyPasswordless, missing code and authCodeResponse", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider6], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.status, 400); - }); - - it("test for thirdPartyPasswordless, minimum config for thirdParty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - EmailVerification.init({ mode: "OPTIONAL", createAndSendCustomEmail: () => {} }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); - - let cookies1 = extractInfoFromResponse(response1); - assert.notStrictEqual(cookies1.accessToken, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.notStrictEqual(cookies1.antiCsrf, undefined); - assert.notStrictEqual(cookies1.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.strictEqual(cookies1.accessTokenDomain, undefined); - assert.strictEqual(cookies1.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies1.frontToken, "remove"); - - assert.strictEqual( - await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), - true - ); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - - assert.notStrictEqual(response2, undefined); - assert.strictEqual(response2.body.status, "OK"); - assert.strictEqual(response2.body.createdNewUser, false); - assert.strictEqual(response2.body.user.thirdParty.id, "custom"); - assert.strictEqual(response2.body.user.thirdParty.userId, "user"); - assert.strictEqual(response2.body.user.email, "email@test.com"); - - let cookies2 = extractInfoFromResponse(response2); - assert.notStrictEqual(cookies2.accessToken, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.notStrictEqual(cookies2.antiCsrf, undefined); - assert.notStrictEqual(cookies2.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies2.refreshToken, undefined); - assert.strictEqual(cookies2.accessTokenDomain, undefined); - assert.strictEqual(cookies2.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies2.frontToken, "remove"); - }); - - it("test with thirdPartyPasswordless, with minimum config for thirdparty module, email unverified", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider5], - }), - EmailVerification.init({ mode: "OPTIONAL", createAndSendCustomEmail: () => {} }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.notStrictEqual(response1, undefined); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.body.createdNewUser, true); - assert.strictEqual(response1.body.user.thirdParty.id, "custom"); - assert.strictEqual(response1.body.user.thirdParty.userId, "user"); - assert.strictEqual(response1.body.user.email, "email@test.com"); - - let cookies1 = extractInfoFromResponse(response1); - assert.notStrictEqual(cookies1.accessToken, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.notStrictEqual(cookies1.antiCsrf, undefined); - assert.notStrictEqual(cookies1.accessTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined); - assert.notStrictEqual(cookies1.refreshToken, undefined); - assert.strictEqual(cookies1.accessTokenDomain, undefined); - assert.strictEqual(cookies1.refreshTokenDomain, undefined); - assert.notStrictEqual(cookies1.frontToken, "remove"); - - assert.strictEqual( - await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), - false - ); - }); - - it("test with thirdPartyPasswordless, thirdparty provider doesn't exist in config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "google", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual( - response1.body.message, - "The third party provider google seems to be missing from the backend configs." - ); - }); - - it("test with thirdPartyPasswordless, provider get function throws error", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider2], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from get function" }); - }); - - it("test with thirdPartyPasswordless, email not returned in getProfileInfo function", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider3], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 200); - assert.strictEqual(response1.body.status, "NO_EMAIL_GIVEN_BY_PROVIDER"); - }); - - it("test with thirdPartyPasswordless, error thrown from getProfileInfo function", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider4], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - app.use((err, request, response, next) => { - response.status(500).send({ - message: err.message, - }); - }); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 500); - assert.deepStrictEqual(response1.body, { message: "error from getProfileInfo" }); - }); - - it("test with thirdPartyPasswordless, invalid POST params for thirdparty module", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - Session.init({ getTokenTransferMethod: () => "cookie" }), - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({}) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.statusCode, 400); - assert.strictEqual(response1.body.message, "Please provide the thirdPartyId in request body"); - - let response2 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response2.statusCode, 400); - assert.strictEqual( - response2.body.message, - "Please provide one of code or authCodeResponse in the request body" - ); - - let response3 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: false, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response3.statusCode, 400); - assert.strictEqual(response3.body.message, "Please provide the thirdPartyId in request body"); - - let response4 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: 12323, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response4.statusCode, 400); - assert.strictEqual(response4.body.message, "Please provide the thirdPartyId in request body"); - - let response5 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: true, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response5.statusCode, 400); - assert.strictEqual(response5.body.message, "Please make sure that the code in the request body is a string"); - - let response6 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: 32432432, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response6.statusCode, 400); - assert.strictEqual(response6.body.message, "Please make sure that the code in the request body is a string"); - - let response7 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response7.statusCode, 400); - assert.strictEqual(response7.body.message, "Please provide the redirectURI in request body"); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response8 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response8.statusCode, 200); - }); - - it("test with thirdPartyPasswordless, getUserById when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - assert.strictEqual(await ThirdPartyPasswordless.getUserById("randomID"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyPasswordless.getUserById(signUpUserInfo.id); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); - - it("test getUserByThirdPartyInfo when user does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - assert.strictEqual(await ThirdPartyPasswordless.getUserByThirdPartyInfo("custom", "user"), undefined); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "32432432", - redirectURI: "http://localhost.org", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 200); - - let signUpUserInfo = response.body.user; - let userInfo = await ThirdPartyPasswordless.getUserByThirdPartyInfo("custom", "user"); - - assert.strictEqual(userInfo.email, signUpUserInfo.email); - assert.strictEqual(userInfo.id, signUpUserInfo.id); - }); -}); diff --git a/test/thirdpartypasswordless/signinupFeature.test.ts b/test/thirdpartypasswordless/signinupFeature.test.ts new file mode 100644 index 000000000..3f02e18ac --- /dev/null +++ b/test/thirdpartypasswordless/signinupFeature.test.ts @@ -0,0 +1,1142 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyPasswordless, { TypeProvider } from 'supertokens-node/recipe/thirdpartypasswordless' +import EmailVerification from 'supertokens-node/recipe/emailverification' +import nock from 'nock' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractInfoFromResponse, + isCDIVersionCompatible, + killAllST, + printPath, + setupST, + startST, +} from '../utils' + +describe(`signinupTest: ${printPath('[test/thirdpartypasswordless/signinupFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + let customProvider2: TypeProvider + let customProvider3: TypeProvider + let customProvider4: TypeProvider + let customProvider5: TypeProvider + let customProvider6: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider2 = { + id: 'custom', + get: (recipe, authCode) => { + throw new Error('error from get function') + }, + } + customProvider3 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider4 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + throw new Error('error from getProfileInfo') + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider5 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: false, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + customProvider6 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + if (authCodeResponse.access_token === undefined) + return {} + + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test with thirdPartyPasswordless, that if you disable the signInUp api, it does not work', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: undefined, + } + }, + }, + providers: [ + ThirdPartyPasswordless.Google({ + clientId: 'test', + clientSecret: 'test-secret', + }), + ], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'google', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.status, 404) + }) + + it('test with thirdPartyPasswordless, minimum config without code for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL', createAndSendCustomEmail: () => {} }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider6], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + authCodeResponse: { + access_token: 'saodiasjodai', + }, + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.createdNewUser, true) + assert.strictEqual(response1.body.user.thirdParty.id, 'custom') + assert.strictEqual(response1.body.user.thirdParty.userId, 'user') + assert.strictEqual(response1.body.user.email, 'email@test.com') + + const cookies1 = extractInfoFromResponse(response1) + assert.notStrictEqual(cookies1.accessToken, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.notStrictEqual(cookies1.antiCsrf, undefined) + assert.notStrictEqual(cookies1.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.strictEqual(cookies1.accessTokenDomain, undefined) + assert.strictEqual(cookies1.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies1.frontToken, 'remove') + + assert.strictEqual( + await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), + true, + ) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + authCodeResponse: { + access_token: 'saodiasjodai', + }, + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + else + resolve(res) + }), + ) + + assert.notStrictEqual(response2, undefined) + assert.strictEqual(response2.body.status, 'OK') + assert.strictEqual(response2.body.createdNewUser, false) + assert.strictEqual(response2.body.user.thirdParty.id, 'custom') + assert.strictEqual(response2.body.user.thirdParty.userId, 'user') + assert.strictEqual(response2.body.user.email, 'email@test.com') + + const cookies2 = extractInfoFromResponse(response2) + assert.notStrictEqual(cookies2.accessToken, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.notStrictEqual(cookies2.antiCsrf, undefined) + assert.notStrictEqual(cookies2.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.strictEqual(cookies2.accessTokenDomain, undefined) + assert.strictEqual(cookies2.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies2.frontToken, 'remove') + }) + + it('test with thirdPartyPasswordless, missing code and authCodeResponse', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider6], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.status, 400) + }) + + it('test for thirdPartyPasswordless, minimum config for thirdParty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + EmailVerification.init({ mode: 'OPTIONAL', createAndSendCustomEmail: () => {} }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.createdNewUser, true) + assert.strictEqual(response1.body.user.thirdParty.id, 'custom') + assert.strictEqual(response1.body.user.thirdParty.userId, 'user') + assert.strictEqual(response1.body.user.email, 'email@test.com') + + const cookies1 = extractInfoFromResponse(response1) + assert.notStrictEqual(cookies1.accessToken, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.notStrictEqual(cookies1.antiCsrf, undefined) + assert.notStrictEqual(cookies1.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.strictEqual(cookies1.accessTokenDomain, undefined) + assert.strictEqual(cookies1.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies1.frontToken, 'remove') + + assert.strictEqual( + await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), + true, + ) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + + assert.notStrictEqual(response2, undefined) + assert.strictEqual(response2.body.status, 'OK') + assert.strictEqual(response2.body.createdNewUser, false) + assert.strictEqual(response2.body.user.thirdParty.id, 'custom') + assert.strictEqual(response2.body.user.thirdParty.userId, 'user') + assert.strictEqual(response2.body.user.email, 'email@test.com') + + const cookies2 = extractInfoFromResponse(response2) + assert.notStrictEqual(cookies2.accessToken, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.notStrictEqual(cookies2.antiCsrf, undefined) + assert.notStrictEqual(cookies2.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies2.refreshToken, undefined) + assert.strictEqual(cookies2.accessTokenDomain, undefined) + assert.strictEqual(cookies2.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies2.frontToken, 'remove') + }) + + it('test with thirdPartyPasswordless, with minimum config for thirdparty module, email unverified', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider5], + }), + EmailVerification.init({ mode: 'OPTIONAL', createAndSendCustomEmail: () => {} }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.notStrictEqual(response1, undefined) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.body.createdNewUser, true) + assert.strictEqual(response1.body.user.thirdParty.id, 'custom') + assert.strictEqual(response1.body.user.thirdParty.userId, 'user') + assert.strictEqual(response1.body.user.email, 'email@test.com') + + const cookies1 = extractInfoFromResponse(response1) + assert.notStrictEqual(cookies1.accessToken, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.notStrictEqual(cookies1.antiCsrf, undefined) + assert.notStrictEqual(cookies1.accessTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshTokenExpiry, undefined) + assert.notStrictEqual(cookies1.refreshToken, undefined) + assert.strictEqual(cookies1.accessTokenDomain, undefined) + assert.strictEqual(cookies1.refreshTokenDomain, undefined) + assert.notStrictEqual(cookies1.frontToken, 'remove') + + assert.strictEqual( + await EmailVerification.isEmailVerified(response1.body.user.id, response1.body.user.email), + false, + ) + }) + + it('test with thirdPartyPasswordless, thirdparty provider doesn\'t exist in config', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'google', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual( + response1.body.message, + 'The third party provider google seems to be missing from the backend configs.', + ) + }) + + it('test with thirdPartyPasswordless, provider get function throws error', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider2], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from get function' }) + }) + + it('test with thirdPartyPasswordless, email not returned in getProfileInfo function', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider3], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 200) + assert.strictEqual(response1.body.status, 'NO_EMAIL_GIVEN_BY_PROVIDER') + }) + + it('test with thirdPartyPasswordless, error thrown from getProfileInfo function', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider4], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + app.use((err, request, response, next) => { + response.status(500).send({ + message: err.message, + }) + }) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 500) + assert.deepStrictEqual(response1.body, { message: 'error from getProfileInfo' }) + }) + + it('test with thirdPartyPasswordless, invalid POST params for thirdparty module', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({}) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.statusCode, 400) + assert.strictEqual(response1.body.message, 'Please provide the thirdPartyId in request body') + + const response2 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response2.statusCode, 400) + assert.strictEqual( + response2.body.message, + 'Please provide one of code or authCodeResponse in the request body', + ) + + const response3 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: false, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response3.statusCode, 400) + assert.strictEqual(response3.body.message, 'Please provide the thirdPartyId in request body') + + const response4 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 12323, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response4.statusCode, 400) + assert.strictEqual(response4.body.message, 'Please provide the thirdPartyId in request body') + + const response5 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: true, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response5.statusCode, 400) + assert.strictEqual(response5.body.message, 'Please make sure that the code in the request body is a string') + + const response6 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 32432432, + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response6.statusCode, 400) + assert.strictEqual(response6.body.message, 'Please make sure that the code in the request body is a string') + + const response7 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response7.statusCode, 400) + assert.strictEqual(response7.body.message, 'Please provide the redirectURI in request body') + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response8 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response8.statusCode, 200) + }) + + it('test with thirdPartyPasswordless, getUserById when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + assert.strictEqual(await ThirdPartyPasswordless.getUserById('randomID'), undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + const userInfo = await ThirdPartyPasswordless.getUserById(signUpUserInfo.id) + + assert.strictEqual(userInfo.email, signUpUserInfo.email) + assert.strictEqual(userInfo.id, signUpUserInfo.id) + }) + + it('test getUserByThirdPartyInfo when user does not exist', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + assert.strictEqual(await ThirdPartyPasswordless.getUserByThirdPartyInfo('custom', 'user'), undefined) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: '32432432', + redirectURI: 'http://localhost.org', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 200) + + const signUpUserInfo = response.body.user + const userInfo = await ThirdPartyPasswordless.getUserByThirdPartyInfo('custom', 'user') + + assert.strictEqual(userInfo.email, signUpUserInfo.email) + assert.strictEqual(userInfo.id, signUpUserInfo.id) + }) +}) diff --git a/test/thirdpartypasswordless/signoutFeature.test.js b/test/thirdpartypasswordless/signoutFeature.test.js deleted file mode 100644 index f4550e095..000000000 --- a/test/thirdpartypasswordless/signoutFeature.test.js +++ /dev/null @@ -1,394 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, - isCDIVersionCompatible, -} = require("../utils"); -let STExpress = require("../../"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdPartyPasswordless = require("../../lib/build/recipe/thirdpartypasswordless"); -let nock = require("nock"); -const express = require("express"); -const request = require("supertest"); -let Session = require("../../recipe/session"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`signoutTest: ${printPath("[test/thirdpartypasswordless/signoutFeature.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: "user", - email: { - id: "email@test.com", - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test the default route and it should revoke the session", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.statusCode, 200); - - let res = extractInfoFromResponse(response1); - - let response2 = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - assert.strictEqual(response2.antiCsrf, undefined); - assert.strictEqual(response2.accessToken, ""); - assert.strictEqual(response2.refreshToken, ""); - assert.strictEqual(response2.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(response2.accessTokenDomain, undefined); - assert.strictEqual(response2.refreshTokenDomain, undefined); - assert.strictEqual(response2.frontToken, "remove"); - }); - - it("test that disabling default route and calling the API returns 404", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - override: { - apis: (oI) => { - return { - ...oI, - thirdPartySignInUpPOST: undefined, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "thirdparty") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.statusCode, 404); - }); - - it("test that calling the API without a session should return OK", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response.body.status, "OK"); - assert.strictEqual(response.status, 200); - assert.strictEqual(response.header["set-cookie"], undefined); - }); - - it("test that signout API reutrns try refresh token, refresh session and signout should return OK", async function () { - await setKeyValueInConfig("access_token_validity", 2); - - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - nock("https://test.com").post("/oauth/token").reply(200, {}); - - let response1 = await new Promise((resolve) => - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(response1.body.status, "OK"); - assert.strictEqual(response1.statusCode, 200); - - let res = extractInfoFromResponse(response1); - - await new Promise((r) => setTimeout(r, 5000)); - - let signOutResponse = await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + res.accessToken]) - .set("anti-csrf", res.antiCsrf) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert.strictEqual(signOutResponse.status, 401); - assert.strictEqual(signOutResponse.body.message, "try refresh token"); - - let refreshedResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/session/refresh") - .set("rid", "session") - .set("Cookie", ["sRefreshToken=" + res.refreshToken]) - .set("anti-csrf", res.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - signOutResponse = extractInfoFromResponse( - await new Promise((resolve) => - request(app) - .post("/auth/signout") - .set("rid", "session") - .set("Cookie", ["sAccessToken=" + refreshedResponse.accessToken]) - .set("anti-csrf", refreshedResponse.antiCsrf) - .expect(200) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ) - ); - - assert.strictEqual(signOutResponse.antiCsrf, undefined); - assert.strictEqual(signOutResponse.accessToken, ""); - assert.strictEqual(signOutResponse.refreshToken, ""); - assert.strictEqual(signOutResponse.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(signOutResponse.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); - assert.strictEqual(signOutResponse.accessTokenDomain, undefined); - assert.strictEqual(signOutResponse.refreshTokenDomain, undefined); - }); -}); diff --git a/test/thirdpartypasswordless/signoutFeature.test.ts b/test/thirdpartypasswordless/signoutFeature.test.ts new file mode 100644 index 000000000..1af6678a6 --- /dev/null +++ b/test/thirdpartypasswordless/signoutFeature.test.ts @@ -0,0 +1,394 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import ThirdPartyPasswordless, { TypeProvider } from 'supertokens-node/recipe/thirdpartypasswordless' +import nock from 'nock' +import express from 'express' +import request from 'supertest' +import Session from 'supertokens-node/recipe/session' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { + cleanST, + extractInfoFromResponse, + isCDIVersionCompatible, + killAllST, + printPath, + setKeyValueInConfig, + setupST, + startST, +} from '../utils' + +describe(`signoutTest: ${printPath('[test/thirdpartypasswordless/signoutFeature.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: 'user', + email: { + id: 'email@test.com', + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test the default route and it should revoke the session', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.statusCode, 200) + + const res = extractInfoFromResponse(response1) + + const response2 = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + assert.strictEqual(response2.antiCsrf, undefined) + assert.strictEqual(response2.accessToken, '') + assert.strictEqual(response2.refreshToken, '') + assert.strictEqual(response2.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(response2.accessTokenDomain, undefined) + assert.strictEqual(response2.refreshTokenDomain, undefined) + assert.strictEqual(response2.frontToken, 'remove') + }) + + it('test that disabling default route and calling the API returns 404', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + override: { + apis: (oI) => { + return { + ...oI, + thirdPartySignInUpPOST: undefined, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'thirdparty') + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.statusCode, 404) + }) + + it('test that calling the API without a session should return OK', async () => { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signout') + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response.body.status, 'OK') + assert.strictEqual(response.status, 200) + assert.strictEqual(response.header['set-cookie'], undefined) + }) + + it('test that signout API reutrns try refresh token, refresh session and signout should return OK', async () => { + await setKeyValueInConfig('access_token_validity', 2) + + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie', antiCsrf: 'VIA_TOKEN' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + nock('https://test.com').post('/oauth/token').reply(200, {}) + + const response1 = await new Promise(resolve => + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(response1.body.status, 'OK') + assert.strictEqual(response1.statusCode, 200) + + const res = extractInfoFromResponse(response1) + + await new Promise(r => setTimeout(r, 5000)) + + let signOutResponse = await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${res.accessToken}`]) + .set('anti-csrf', res.antiCsrf) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert.strictEqual(signOutResponse.status, 401) + assert.strictEqual(signOutResponse.body.message, 'try refresh token') + + const refreshedResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/session/refresh') + .set('rid', 'session') + .set('Cookie', [`sRefreshToken=${res.refreshToken}`]) + .set('anti-csrf', res.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + signOutResponse = extractInfoFromResponse( + await new Promise(resolve => + request(app) + .post('/auth/signout') + .set('rid', 'session') + .set('Cookie', [`sAccessToken=${refreshedResponse.accessToken}`]) + .set('anti-csrf', refreshedResponse.antiCsrf) + .expect(200) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ), + ) + + assert.strictEqual(signOutResponse.antiCsrf, undefined) + assert.strictEqual(signOutResponse.accessToken, '') + assert.strictEqual(signOutResponse.refreshToken, '') + assert.strictEqual(signOutResponse.accessTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(signOutResponse.refreshTokenExpiry, 'Thu, 01 Jan 1970 00:00:00 GMT') + assert.strictEqual(signOutResponse.accessTokenDomain, undefined) + assert.strictEqual(signOutResponse.refreshTokenDomain, undefined) + }) +}) diff --git a/test/thirdpartypasswordless/smsDelivery.test.js b/test/thirdpartypasswordless/smsDelivery.test.js deleted file mode 100644 index d464794cd..000000000 --- a/test/thirdpartypasswordless/smsDelivery.test.js +++ /dev/null @@ -1,1277 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { printPath, setupST, startST, killAllST, cleanST, delay } = require("../utils"); -let STExpress = require("../.."); -let Session = require("../../recipe/session"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let ThirdpartyPasswordless = require("../../recipe/thirdpartypasswordless"); -let { TwilioService, SupertokensService } = require("../../recipe/thirdpartypasswordless/smsdelivery"); -let nock = require("nock"); -let supertest = require("supertest"); -const { middleware, errorHandler } = require("../../framework/express"); -let express = require("express"); -let { isCDIVersionCompatible } = require("../utils"); - -describe(`smsDelivery: ${printPath("[test/thirdpartypasswordless/smsDelivery.test.js]")}`, function () { - beforeEach(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - process.env.TEST_MODE = "testing"; - await killAllST(); - await cleanST(); - }); - - it("test default backward compatibility api being called: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let apiKey = "randomKey"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - return {}; - }); - - process.env.TEST_MODE = "production"; - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert.strictEqual(apiKey, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: async (input) => { - phoneNumber = input.phoneNumber; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - override: (oI) => { - return { - sendSms: async (input) => { - phoneNumber = input.phoneNumber; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test twilio service: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let sendRawSmsCalled = false; - let getContentCalled = true; - let twilioAPICalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new TwilioService({ - twilioSettings: { - accountSid: "ACTWILIO_ACCOUNT_SID", - authToken: "test-token", - from: "+919909909999", - }, - override: (oI) => { - return { - sendRawSms: async (input) => { - sendRawSmsCalled = true; - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.toPhoneNumber, "+919909909998"); - phoneNumber = input.toPhoneNumber; - await oI.sendRawSms(input); - }, - getContent: async (input) => { - getContentCalled = true; - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - userInputCode = input.userInputCode; - urlWithLinkCode = input.urlWithLinkCode; - codeLifetime = input.codeLifetime; - return { - body: input.userInputCode, - toPhoneNumber: input.phoneNumber, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.twilio.com/2010-04-01") - .post("/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json") - .reply(200, (uri, body) => { - twilioAPICalled = true; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert(outerOverrideCalled); - assert(sendRawSmsCalled); - assert(getContentCalled); - assert(twilioAPICalled); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(500, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); - - it("test supertokens service: passwordless login", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let supertokensAPICalled = false; - let apiKey = undefined; - let type = undefined; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new SupertokensService("API_KEY"), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - supertokensAPICalled = true; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - type = body.smsInput.type; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert(outerOverrideCalled); - assert(supertokensAPICalled); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(apiKey, "API_KEY"); - }); - - it("test default backward compatibility api being called, error message not sent back to user if response code is 429: passwordless login", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(429, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let result = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.body.status, "OK"); - }); - - it("test default backward compatibility api being called: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - let apiKey = "randomKey"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert.strictEqual(apiKey, undefined); - assert(codeLifetime > 0); - }); - - it("test backward compatibility: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let sendCustomSMSCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomTextMessage: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendCustomSMSCalled) { - phoneNumber = input.phoneNumber; - codeLifetime = input.codeLifetime; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - } - sendCustomSMSCalled = true; - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - await delay(2); - assert.strictEqual(sendCustomSMSCalled, true); - assert.strictEqual(phoneNumber, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test custom override: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let type = undefined; - let appName = undefined; - let overrideCalled = false; - let loginCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - override: (oI) => { - return { - sendSms: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (overrideCalled) { - phoneNumber = input.phoneNumber; - urlWithLinkCode = input.urlWithLinkCode; - userInputCode = input.userInputCode; - codeLifetime = input.codeLifetime; - type = input.type; - } - overrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - process.env.TEST_MODE = "production"; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - assert.strictEqual(overrideCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - appName = body.smsInput.appName; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test twilio service: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let urlWithLinkCode = undefined; - let outerOverrideCalled = false; - let sendRawSmsCalled = false; - let getContentCalled = false; - let loginCalled = false; - let twilioAPICalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new TwilioService({ - twilioSettings: { - accountSid: "ACTWILIO_ACCOUNT_SID", - authToken: "test-token", - from: "+919909909999", - }, - override: (oI) => { - return { - sendRawSms: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (sendRawSmsCalled) { - assert.strictEqual(input.body, userInputCode); - assert.strictEqual(input.toPhoneNumber, "+919909909998"); - phoneNumber = input.toPhoneNumber; - } - sendRawSmsCalled = true; - await oI.sendRawSms(input); - }, - getContent: async (input) => { - /** - * when the function is called for the first time, - * it will be for signinup - */ - if (getContentCalled) { - userInputCode = input.userInputCode; - urlWithLinkCode = input.urlWithLinkCode; - codeLifetime = input.codeLifetime; - } - assert.strictEqual(input.type, "PASSWORDLESS_LOGIN"); - getContentCalled = true; - return { - body: input.userInputCode, - toPhoneNumber: input.phoneNumber, - }; - }, - }; - }, - }), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.twilio.com/2010-04-01") - .post("/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert(getContentCalled); - assert(sendRawSmsCalled); - assert(loginCalled); - assert.strictEqual(phoneNumber, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - - nock("https://api.twilio.com/2010-04-01") - .post("/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json") - .reply(200, (uri, body) => { - twilioAPICalled = true; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - await delay(2); - assert.strictEqual(phoneNumber, "+919909909998"); - assert(outerOverrideCalled); - assert(twilioAPICalled); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - }); - - it("test default backward compatibility api being called, error message sent back to user: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - let message = ""; - app.use((err, req, res, next) => { - message = err.message; - res.status(500).send(message); - }); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - assert.strictEqual(appName, undefined); - assert.strictEqual(phoneNumber, undefined); - assert.strictEqual(urlWithLinkCode, undefined); - assert.strictEqual(userInputCode, undefined); - assert.strictEqual(codeLifetime, undefined); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(500, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - let result = await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(500); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.status, 500); - assert(message === "Request failed with status code 500"); - }); - - it("test supertokens service: resend code api", async function () { - await startST(); - let phoneNumber = undefined; - let codeLifetime = undefined; - let userInputCode = undefined; - let outerOverrideCalled = false; - let supertokensAPICalled = false; - let apiKey = undefined; - let type = undefined; - let loginCalled = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - smsDelivery: { - service: new SupertokensService("API_KEY"), - override: (oI) => { - return { - sendSms: async (input) => { - outerOverrideCalled = true; - await oI.sendSms(input); - }, - }; - }, - }, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - - await delay(2); - assert(loginCalled); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - supertokensAPICalled = true; - userInputCode = body.smsInput.userInputCode; - apiKey = body.apiKey; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - type = body.smsInput.type; - return {}; - }); - - await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - await delay(2); - - assert.strictEqual(phoneNumber, "+919909909998"); - assert.strictEqual(type, "PASSWORDLESS_LOGIN"); - assert(outerOverrideCalled); - assert(supertokensAPICalled); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(apiKey, "API_KEY"); - }); - - it("test default backward compatibility api being called, error message not sent back to user if response code is 429: resend code api", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdpartyPasswordless.init({ - contactMethod: "PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - telemetry: false, - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const app = express(); - app.use(express.json()); - app.use(middleware()); - app.use(errorHandler()); - - let appName = undefined; - let phoneNumber = undefined; - let codeLifetime = undefined; - let urlWithLinkCode = undefined; - let userInputCode = undefined; - let loginCalled = false; - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(200, (uri, body) => { - loginCalled = true; - return {}; - }); - - process.env.TEST_MODE = "production"; - - let response = await supertest(app) - .post("/auth/signinup/code") - .set("rid", "thirdpartypasswordless") - .send({ - phoneNumber: "+919909909998", - }) - .expect(200); - assert.strictEqual(loginCalled, true); - - nock("https://api.supertokens.com") - .post("/0/services/sms") - .reply(429, (uri, body) => { - appName = body.smsInput.appName; - phoneNumber = body.smsInput.phoneNumber; - codeLifetime = body.smsInput.codeLifetime; - urlWithLinkCode = body.smsInput.urlWithLinkCode; - userInputCode = body.smsInput.userInputCode; - return {}; - }); - - let result = await supertest(app) - .post("/auth/signinup/code/resend") - .set("rid", "thirdpartypasswordless") - .send({ - deviceId: response.body.deviceId, - preAuthSessionId: response.body.preAuthSessionId, - }) - .expect(200); - - process.env.TEST_MODE = "testing"; - - assert.strictEqual(appName, "SuperTokens"); - assert.strictEqual(phoneNumber, "+919909909998"); - assert.notStrictEqual(urlWithLinkCode, undefined); - assert.notStrictEqual(userInputCode, undefined); - assert.notStrictEqual(codeLifetime, undefined); - assert(codeLifetime > 0); - assert.strictEqual(result.body.status, "OK"); - }); -}); diff --git a/test/thirdpartypasswordless/smsDelivery.test.ts b/test/thirdpartypasswordless/smsDelivery.test.ts new file mode 100644 index 000000000..e610fd551 --- /dev/null +++ b/test/thirdpartypasswordless/smsDelivery.test.ts @@ -0,0 +1,1264 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import Session from 'supertokens-node/recipe/session' +import { ProcessState } from 'supertokens-node/processState' +import ThirdpartyPasswordless from 'supertokens-node/recipe/thirdpartypasswordless' +import nock from 'nock' +import supertest from 'supertest' +import STExpress from 'supertokens-node' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { SupertokensService, TwilioService } from 'supertokens-node/recipe/thirdpartypasswordless/smsdelivery' +import express from 'express' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, delay, isCDIVersionCompatible, killAllST, printPath, setupST, startST } from '../utils' + +describe(`smsDelivery: ${printPath('[test/thirdpartypasswordless/smsDelivery.test.ts]')}`, () => { + beforeEach(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + process.env.TEST_MODE = 'testing' + await killAllST() + await cleanST() + }) + + it('test default backward compatibility api being called: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let apiKey = 'randomKey' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + return {} + }) + + process.env.TEST_MODE = 'production' + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert.strictEqual(apiKey, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: async (input) => { + phoneNumber = input.phoneNumber + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + override: (oI) => { + return { + sendSms: async (input) => { + phoneNumber = input.phoneNumber + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test twilio service: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let outerOverrideCalled = false + let sendRawSmsCalled = false + let getContentCalled = true + let twilioAPICalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new TwilioService({ + twilioSettings: { + accountSid: 'ACTWILIO_ACCOUNT_SID', + authToken: 'test-token', + from: '+919909909999', + }, + override: (oI) => { + return { + sendRawSms: async (input) => { + sendRawSmsCalled = true + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.toPhoneNumber, '+919909909998') + phoneNumber = input.toPhoneNumber + await oI.sendRawSms(input) + }, + getContent: async (input) => { + getContentCalled = true + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + userInputCode = input.userInputCode + urlWithLinkCode = input.urlWithLinkCode + codeLifetime = input.codeLifetime + return { + body: input.userInputCode, + toPhoneNumber: input.phoneNumber, + } + }, + } + }, + }), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.twilio.com/2010-04-01') + .post('/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json') + .reply(200, (uri, body) => { + twilioAPICalled = true + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert(outerOverrideCalled) + assert(sendRawSmsCalled) + assert(getContentCalled) + assert(twilioAPICalled) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(500, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) + + it('test supertokens service: passwordless login', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let outerOverrideCalled = false + let supertokensAPICalled = false + let apiKey + let type + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new SupertokensService('API_KEY'), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + supertokensAPICalled = true + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + type = body.smsInput.type + return {} + }) + + await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert(outerOverrideCalled) + assert(supertokensAPICalled) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(apiKey, 'API_KEY') + }) + + it('test default backward compatibility api being called, error message not sent back to user if response code is 429: passwordless login', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(429, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + process.env.TEST_MODE = 'production' + + const result = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.body.status, 'OK') + }) + + it('test default backward compatibility api being called: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + let apiKey = 'randomKey' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert.strictEqual(apiKey, undefined) + assert(codeLifetime > 0) + }) + + it('test backward compatibility: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let sendCustomSMSCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomTextMessage: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendCustomSMSCalled) { + phoneNumber = input.phoneNumber + codeLifetime = input.codeLifetime + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + } + sendCustomSMSCalled = true + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + await delay(2) + assert.strictEqual(sendCustomSMSCalled, true) + assert.strictEqual(phoneNumber, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test custom override: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let type + let appName + let overrideCalled = false + let loginCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + override: (oI) => { + return { + sendSms: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (overrideCalled) { + phoneNumber = input.phoneNumber + urlWithLinkCode = input.urlWithLinkCode + userInputCode = input.userInputCode + codeLifetime = input.codeLifetime + type = input.type + } + overrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + process.env.TEST_MODE = 'production' + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + assert.strictEqual(overrideCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + appName = body.smsInput.appName + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test twilio service: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let urlWithLinkCode + let outerOverrideCalled = false + let sendRawSmsCalled = false + let getContentCalled = false + let loginCalled = false + let twilioAPICalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new TwilioService({ + twilioSettings: { + accountSid: 'ACTWILIO_ACCOUNT_SID', + authToken: 'test-token', + from: '+919909909999', + }, + override: (oI) => { + return { + sendRawSms: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (sendRawSmsCalled) { + assert.strictEqual(input.body, userInputCode) + assert.strictEqual(input.toPhoneNumber, '+919909909998') + phoneNumber = input.toPhoneNumber + } + sendRawSmsCalled = true + await oI.sendRawSms(input) + }, + getContent: async (input) => { + /** + * when the function is called for the first time, + * it will be for signinup + */ + if (getContentCalled) { + userInputCode = input.userInputCode + urlWithLinkCode = input.urlWithLinkCode + codeLifetime = input.codeLifetime + } + assert.strictEqual(input.type, 'PASSWORDLESS_LOGIN') + getContentCalled = true + return { + body: input.userInputCode, + toPhoneNumber: input.phoneNumber, + } + }, + } + }, + }), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.twilio.com/2010-04-01') + .post('/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert(getContentCalled) + assert(sendRawSmsCalled) + assert(loginCalled) + assert.strictEqual(phoneNumber, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + + nock('https://api.twilio.com/2010-04-01') + .post('/Accounts/ACTWILIO_ACCOUNT_SID/Messages.json') + .reply(200, (uri, body) => { + twilioAPICalled = true + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + await delay(2) + assert.strictEqual(phoneNumber, '+919909909998') + assert(outerOverrideCalled) + assert(twilioAPICalled) + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + }) + + it('test default backward compatibility api being called, error message sent back to user: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + let message = '' + app.use((err, req, res, next) => { + message = err.message + res.status(500).send(message) + }) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + assert.strictEqual(appName, undefined) + assert.strictEqual(phoneNumber, undefined) + assert.strictEqual(urlWithLinkCode, undefined) + assert.strictEqual(userInputCode, undefined) + assert.strictEqual(codeLifetime, undefined) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(500, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + const result = await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(500) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.status, 500) + assert(message === 'Request failed with status code 500') + }) + + it('test supertokens service: resend code api', async () => { + await startST() + let phoneNumber + let codeLifetime + let userInputCode + let outerOverrideCalled = false + let supertokensAPICalled = false + let apiKey + let type + let loginCalled = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + smsDelivery: { + service: new SupertokensService('API_KEY'), + override: (oI) => { + return { + sendSms: async (input) => { + outerOverrideCalled = true + await oI.sendSms(input) + }, + } + }, + }, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + + await delay(2) + assert(loginCalled) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + supertokensAPICalled = true + userInputCode = body.smsInput.userInputCode + apiKey = body.apiKey + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + type = body.smsInput.type + return {} + }) + + await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + await delay(2) + + assert.strictEqual(phoneNumber, '+919909909998') + assert.strictEqual(type, 'PASSWORDLESS_LOGIN') + assert(outerOverrideCalled) + assert(supertokensAPICalled) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(apiKey, 'API_KEY') + }) + + it('test default backward compatibility api being called, error message not sent back to user if response code is 429: resend code api', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdpartyPasswordless.init({ + contactMethod: 'PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + telemetry: false, + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const app = express() + app.use(express.json()) + app.use(middleware()) + app.use(errorHandler()) + + let appName + let phoneNumber + let codeLifetime + let urlWithLinkCode + let userInputCode + let loginCalled = false + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(200, (uri, body) => { + loginCalled = true + return {} + }) + + process.env.TEST_MODE = 'production' + + const response = await supertest(app) + .post('/auth/signinup/code') + .set('rid', 'thirdpartypasswordless') + .send({ + phoneNumber: '+919909909998', + }) + .expect(200) + assert.strictEqual(loginCalled, true) + + nock('https://api.supertokens.com') + .post('/0/services/sms') + .reply(429, (uri, body) => { + appName = body.smsInput.appName + phoneNumber = body.smsInput.phoneNumber + codeLifetime = body.smsInput.codeLifetime + urlWithLinkCode = body.smsInput.urlWithLinkCode + userInputCode = body.smsInput.userInputCode + return {} + }) + + const result = await supertest(app) + .post('/auth/signinup/code/resend') + .set('rid', 'thirdpartypasswordless') + .send({ + deviceId: response.body.deviceId, + preAuthSessionId: response.body.preAuthSessionId, + }) + .expect(200) + + process.env.TEST_MODE = 'testing' + + assert.strictEqual(appName, 'SuperTokens') + assert.strictEqual(phoneNumber, '+919909909998') + assert.notStrictEqual(urlWithLinkCode, undefined) + assert.notStrictEqual(userInputCode, undefined) + assert.notStrictEqual(codeLifetime, undefined) + assert(codeLifetime > 0) + assert.strictEqual(result.body.status, 'OK') + }) +}) diff --git a/test/thirdpartypasswordless/users.test.js b/test/thirdpartypasswordless/users.test.js deleted file mode 100644 index ac4dd3ec1..000000000 --- a/test/thirdpartypasswordless/users.test.js +++ /dev/null @@ -1,281 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - signInUPCustomRequest, - isCDIVersionCompatible, -} = require("../utils"); -const { getUserCount, getUsersNewestFirst, getUsersOldestFirst } = require("../../lib/build/"); -let assert = require("assert"); -let { ProcessState } = require("../../lib/build/processState"); -let STExpress = require("../../"); -let Session = require("../../recipe/session"); -let ThirdPartyPasswordless = require("../../lib/build/recipe/thirdpartypasswordless"); -let { middleware, errorHandler } = require("../../framework/express"); - -describe(`usersTest: ${printPath("[test/thirdpartypasswordless/users.test.js]")}`, function () { - before(function () { - this.customProvider1 = { - id: "custom", - get: (recipe, authCode) => { - return { - accessTokenAPI: { - url: "https://test.com/oauth/token", - }, - authorisationRedirect: { - url: "https://test.com/oauth/auth", - }, - getProfileInfo: async (authCodeResponse) => { - return { - id: authCodeResponse.id, - email: { - id: authCodeResponse.email, - isVerified: true, - }, - }; - }, - getClientId: () => { - return "supertokens"; - }, - }; - }, - }; - }); - - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("test getUsersOldestFirst", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signInUPCustomRequest(app, "test@gmail.com", "testPass0"); - await signInUPCustomRequest(app, "test1@gmail.com", "testPass1"); - await signInUPCustomRequest(app, "test2@gmail.com", "testPass2"); - await signInUPCustomRequest(app, "test3@gmail.com", "testPass3"); - await signInUPCustomRequest(app, "test4@gmail.com", "testPass4"); - - let users = await getUsersOldestFirst(); - assert.strictEqual(users.users.length, 5); - assert.strictEqual(users.nextPaginationToken, undefined); - - users = await getUsersOldestFirst({ limit: 1 }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersOldestFirst({ limit: 1, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test1@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersOldestFirst({ limit: 5, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 3); - assert.strictEqual(users.nextPaginationToken, undefined); - - try { - await getUsersOldestFirst({ limit: 10, paginationToken: "invalid-pagination-token" }); - assert(false); - } catch (err) { - if (!err.message.includes("invalid pagination token")) { - throw err; - } - } - - try { - await getUsersOldestFirst({ limit: -1 }); - assert(false); - } catch (err) { - if (!err.message.includes("limit must a positive integer with min value 1")) { - throw err; - } - } - }); - - it("test getUsersNewestFirst", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signInUPCustomRequest(app, "test@gmail.com", "testPass0"); - await signInUPCustomRequest(app, "test1@gmail.com", "testPass1"); - await signInUPCustomRequest(app, "test2@gmail.com", "testPass2"); - await signInUPCustomRequest(app, "test3@gmail.com", "testPass3"); - await signInUPCustomRequest(app, "test4@gmail.com", "testPass4"); - - let users = await getUsersNewestFirst(); - assert.strictEqual(users.users.length, 5); - assert.strictEqual(users.nextPaginationToken, undefined); - - users = await getUsersNewestFirst({ limit: 1 }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test4@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersNewestFirst({ limit: 1, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 1); - assert.strictEqual(users.users[0].user.email, "test3@gmail.com"); - assert.strictEqual(typeof users.nextPaginationToken, "string"); - - users = await getUsersNewestFirst({ limit: 5, paginationToken: users.nextPaginationToken }); - assert.strictEqual(users.users.length, 3); - assert.strictEqual(users.nextPaginationToken, undefined); - - try { - await getUsersOldestFirst({ limit: 10, paginationToken: "invalid-pagination-token" }); - assert(false); - } catch (err) { - if (!err.message.includes("invalid pagination token")) { - throw err; - } - } - - try { - await getUsersOldestFirst({ limit: -1 }); - assert(false); - } catch (err) { - if (!err.message.includes("limit must a positive integer with min value 1")) { - throw err; - } - } - }); - - it("test getUserCount", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordless.init({ - contactMethod: "EMAIL", - createAndSendCustomEmail: (input) => { - return; - }, - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - providers: [this.customProvider1], - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // run test if current CDI version >= 2.11 - if (!(await isCDIVersionCompatible("2.11"))) { - return; - } - - let userCount = await getUserCount(); - assert.strictEqual(userCount, 0); - - const express = require("express"); - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await signInUPCustomRequest(app, "test@gmail.com", "testPass0"); - userCount = await getUserCount(); - assert.strictEqual(userCount, 1); - - await signInUPCustomRequest(app, "test1@gmail.com", "testPass1"); - await signInUPCustomRequest(app, "test2@gmail.com", "testPass2"); - await signInUPCustomRequest(app, "test3@gmail.com", "testPass3"); - await signInUPCustomRequest(app, "test4@gmail.com", "testPass4"); - - userCount = await getUserCount(); - assert.strictEqual(userCount, 5); - }); -}); diff --git a/test/thirdpartypasswordless/users.test.ts b/test/thirdpartypasswordless/users.test.ts new file mode 100644 index 000000000..cf22c8685 --- /dev/null +++ b/test/thirdpartypasswordless/users.test.ts @@ -0,0 +1,280 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest' +import { ProcessState } from 'supertokens-node/processState' +import STExpress, { getUserCount, getUsersNewestFirst, getUsersOldestFirst } from 'supertokens-node' +import Session from 'supertokens-node/recipe/session' +import ThirdPartyPasswordless, { TypeProvider } from 'supertokens-node/recipe/thirdpartypasswordless' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import { + cleanST, + isCDIVersionCompatible, + killAllST, + printPath, + setupST, + signInUPCustomRequest, + startST, +} from '../utils' + +describe(`usersTest: ${printPath('[test/thirdpartypasswordless/users.test.ts]')}`, () => { + let customProvider1: TypeProvider + beforeAll(() => { + customProvider1 = { + id: 'custom', + get: (recipe, authCode) => { + return { + accessTokenAPI: { + url: 'https://test.com/oauth/token', + }, + authorisationRedirect: { + url: 'https://test.com/oauth/auth', + }, + getProfileInfo: async (authCodeResponse) => { + return { + id: authCodeResponse.id, + email: { + id: authCodeResponse.email, + isVerified: true, + }, + } + }, + getClientId: () => { + return 'supertokens' + }, + } + }, + } + }) + + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('test getUsersOldestFirst', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signInUPCustomRequest(app, 'test@gmail.com', 'testPass0') + await signInUPCustomRequest(app, 'test1@gmail.com', 'testPass1') + await signInUPCustomRequest(app, 'test2@gmail.com', 'testPass2') + await signInUPCustomRequest(app, 'test3@gmail.com', 'testPass3') + await signInUPCustomRequest(app, 'test4@gmail.com', 'testPass4') + + let users = await getUsersOldestFirst() + assert.strictEqual(users.users.length, 5) + assert.strictEqual(users.nextPaginationToken, undefined) + + users = await getUsersOldestFirst({ limit: 1 }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersOldestFirst({ limit: 1, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test1@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersOldestFirst({ limit: 5, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 3) + assert.strictEqual(users.nextPaginationToken, undefined) + + try { + await getUsersOldestFirst({ limit: 10, paginationToken: 'invalid-pagination-token' }) + assert(false) + } + catch (err) { + if (!err.message.includes('invalid pagination token')) + throw err + } + + try { + await getUsersOldestFirst({ limit: -1 }) + assert(false) + } + catch (err) { + if (!err.message.includes('limit must a positive integer with min value 1')) + throw err + } + }) + + it('test getUsersNewestFirst', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signInUPCustomRequest(app, 'test@gmail.com', 'testPass0') + await signInUPCustomRequest(app, 'test1@gmail.com', 'testPass1') + await signInUPCustomRequest(app, 'test2@gmail.com', 'testPass2') + await signInUPCustomRequest(app, 'test3@gmail.com', 'testPass3') + await signInUPCustomRequest(app, 'test4@gmail.com', 'testPass4') + + let users = await getUsersNewestFirst() + assert.strictEqual(users.users.length, 5) + assert.strictEqual(users.nextPaginationToken, undefined) + + users = await getUsersNewestFirst({ limit: 1 }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test4@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersNewestFirst({ limit: 1, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 1) + assert.strictEqual(users.users[0].user.email, 'test3@gmail.com') + assert.strictEqual(typeof users.nextPaginationToken, 'string') + + users = await getUsersNewestFirst({ limit: 5, paginationToken: users.nextPaginationToken }) + assert.strictEqual(users.users.length, 3) + assert.strictEqual(users.nextPaginationToken, undefined) + + try { + await getUsersOldestFirst({ limit: 10, paginationToken: 'invalid-pagination-token' }) + assert(false) + } + catch (err) { + if (!err.message.includes('invalid pagination token')) + throw err + } + + try { + await getUsersOldestFirst({ limit: -1 }) + assert(false) + } + catch (err) { + if (!err.message.includes('limit must a positive integer with min value 1')) + throw err + } + }) + + it('test getUserCount', async () => { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordless.init({ + contactMethod: 'EMAIL', + createAndSendCustomEmail: (input) => { + + }, + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + providers: [customProvider1], + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // run test if current CDI version >= 2.11 + if (!(await isCDIVersionCompatible('2.11'))) + return + + let userCount = await getUserCount() + assert.strictEqual(userCount, 0) + + const express = require('express') + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await signInUPCustomRequest(app, 'test@gmail.com', 'testPass0') + userCount = await getUserCount() + assert.strictEqual(userCount, 1) + + await signInUPCustomRequest(app, 'test1@gmail.com', 'testPass1') + await signInUPCustomRequest(app, 'test2@gmail.com', 'testPass2') + await signInUPCustomRequest(app, 'test3@gmail.com', 'testPass3') + await signInUPCustomRequest(app, 'test4@gmail.com', 'testPass4') + + userCount = await getUserCount() + assert.strictEqual(userCount, 5) + }) +}) diff --git a/test/userContext.test.js b/test/userContext.test.js deleted file mode 100644 index 3205e9fb6..000000000 --- a/test/userContext.test.js +++ /dev/null @@ -1,286 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { - printPath, - setupST, - startST, - killAllST, - cleanST, - extractInfoFromResponse, - setKeyValueInConfig, -} = require("./utils"); -let assert = require("assert"); -const express = require("express"); -const request = require("supertest"); -let { ProcessState, PROCESS_STATE } = require("../lib/build/processState"); -let SuperTokens = require(".."); -let Session = require("../recipe/session"); -let EmailPassword = require("../recipe/emailpassword"); -let { Querier } = require("../lib/build/querier"); -const { default: NormalisedURLPath } = require("../lib/build/normalisedURLPath"); -const { verifySession } = require("../recipe/session/framework/express"); -const { default: next } = require("next"); -let { middleware, errorHandler } = require("../framework/express"); -let STExpress = require("../"); - -describe(`userContext: ${printPath("[test/userContext.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - it("testing context across interface and recipe function", async function () { - await startST(); - let works = false; - let signUpContextWorks = false; - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - functions: (oI) => { - return { - ...oI, - signUp: async function (input) { - if (input.userContext.manualCall) { - signUpContextWorks = true; - } - return oI.signUp(input); - }, - signIn: async function (input) { - if (input.userContext.preSignInPOST) { - input.userContext.preSignIn = true; - } - - let resp = await oI.signIn(input); - - if (input.userContext.preSignInPOST && input.userContext.preSignIn) { - input.userContext.postSignIn = true; - } - return resp; - }, - }; - }, - apis: (oI) => { - return { - ...oI, - signInPOST: async function (input) { - input.userContext = { - preSignInPOST: true, - }; - - let resp = await oI.signInPOST(input); - - if ( - input.userContext.preSignInPOST && - input.userContext.preSignIn && - input.userContext.preCreateNewSession && - input.userContext.postCreateNewSession && - input.userContext.postSignIn - ) { - works = true; - } - return resp; - }, - }; - }, - }, - }), - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => { - return { - ...oI, - createNewSession: async function (input) { - if ( - input.userContext.preSignInPOST && - input.userContext.preSignIn && - input.userContext.postSignIn - ) { - input.userContext.preCreateNewSession = true; - } - - let resp = oI.createNewSession(input); - - if ( - input.userContext.preSignInPOST && - input.userContext.preSignIn && - input.userContext.preCreateNewSession && - input.userContext.postSignIn - ) { - input.userContext.postCreateNewSession = true; - } - - return resp; - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await EmailPassword.signUp("random@gmail.com", "validpass123", { - manualCall: true, - }); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 200); - assert(works && signUpContextWorks); - }); - - it("testing default context across interface and recipe function", async function () { - await startST(); - let signInContextWorks = false; - let signInAPIContextWorks = false; - let createNewSessionContextWorks = false; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init({ - override: { - functions: (oI) => { - return { - ...oI, - signIn: async function (input) { - if (input.userContext._default && input.userContext._default.request) { - signInContextWorks = true; - } - - return await oI.signIn(input); - }, - }; - }, - apis: (oI) => { - return { - ...oI, - signInPOST: async function (input) { - if (input.userContext._default && input.userContext._default.request) { - signInAPIContextWorks = true; - } - - return await oI.signInPOST(input); - }, - }; - }, - }, - }), - Session.init({ - getTokenTransferMethod: () => "cookie", - override: { - functions: (oI) => { - return { - ...oI, - createNewSession: async function (input) { - if (input.userContext._default && input.userContext._default.request) { - createNewSessionContextWorks = true; - } - - return await oI.createNewSession(input); - }, - }; - }, - }, - }), - ], - }); - - const app = express(); - - app.use(middleware()); - - app.use(errorHandler()); - - await EmailPassword.signUp("random@gmail.com", "validpass123", { - manualCall: true, - }); - - let response = await new Promise((resolve) => - request(app) - .post("/auth/signin") - .send({ - formFields: [ - { - id: "password", - value: "validpass123", - }, - { - id: "email", - value: "random@gmail.com", - }, - ], - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }) - ); - assert(response.status === 200); - assert(signInContextWorks && signInAPIContextWorks && createNewSessionContextWorks); - }); -}); diff --git a/test/userContext.test.ts b/test/userContext.test.ts new file mode 100644 index 000000000..3ebcc5add --- /dev/null +++ b/test/userContext.test.ts @@ -0,0 +1,275 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import assert from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import express from 'express' +import request from 'supertest' +import { ProcessState } from 'supertokens-node/processState' +import Session from 'supertokens-node/recipe/session' +import EmailPassword from 'supertokens-node/recipe/emailpassword' +import { errorHandler, middleware } from 'supertokens-node/framework/express' +import STExpress from 'supertokens-node' +import { + cleanST, + killAllST, + printPath, + setupST, + startST, +} from './utils' + +describe(`userContext: ${printPath('[test/userContext.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + it('testing context across interface and recipe function', async () => { + await startST() + let works = false + let signUpContextWorks = false + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (oI) => { + return { + ...oI, + async signUp(input) { + if (input.userContext.manualCall) + signUpContextWorks = true + + return oI.signUp(input) + }, + async signIn(input) { + if (input.userContext.preSignInPOST) + input.userContext.preSignIn = true + + const resp = await oI.signIn(input) + + if (input.userContext.preSignInPOST && input.userContext.preSignIn) + input.userContext.postSignIn = true + + return resp + }, + } + }, + apis: (oI) => { + return { + ...oI, + async signInPOST(input) { + input.userContext = { + preSignInPOST: true, + } + + const resp = await oI.signInPOST(input) + + if ( + input.userContext.preSignInPOST + && input.userContext.preSignIn + && input.userContext.preCreateNewSession + && input.userContext.postCreateNewSession + && input.userContext.postSignIn + ) + works = true + + return resp + }, + } + }, + }, + }), + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: (oI) => { + return { + ...oI, + async createNewSession(input) { + if ( + input.userContext.preSignInPOST + && input.userContext.preSignIn + && input.userContext.postSignIn + ) + input.userContext.preCreateNewSession = true + + const resp = oI.createNewSession(input) + + if ( + input.userContext.preSignInPOST + && input.userContext.preSignIn + && input.userContext.preCreateNewSession + && input.userContext.postSignIn + ) + input.userContext.postCreateNewSession = true + + return resp + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await EmailPassword.signUp('random@gmail.com', 'validpass123', { + manualCall: true, + }) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 200) + assert(works && signUpContextWorks) + }) + + it('testing default context across interface and recipe function', async () => { + await startST() + let signInContextWorks = false + let signInAPIContextWorks = false + let createNewSessionContextWorks = false + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (oI) => { + return { + ...oI, + async signIn(input) { + if (input.userContext._default && input.userContext._default.request) + signInContextWorks = true + + return await oI.signIn(input) + }, + } + }, + apis: (oI) => { + return { + ...oI, + async signInPOST(input) { + if (input.userContext._default && input.userContext._default.request) + signInAPIContextWorks = true + + return await oI.signInPOST(input) + }, + } + }, + }, + }), + Session.init({ + getTokenTransferMethod: () => 'cookie', + override: { + functions: (oI) => { + return { + ...oI, + async createNewSession(input) { + if (input.userContext._default && input.userContext._default.request) + createNewSessionContextWorks = true + + return await oI.createNewSession(input) + }, + } + }, + }, + }), + ], + }) + + const app = express() + + app.use(middleware()) + + app.use(errorHandler()) + + await EmailPassword.signUp('random@gmail.com', 'validpass123', { + manualCall: true, + }) + + const response = await new Promise(resolve => + request(app) + .post('/auth/signin') + .send({ + formFields: [ + { + id: 'password', + value: 'validpass123', + }, + { + id: 'email', + value: 'random@gmail.com', + }, + ], + }) + .end((err, res) => { + if (err) + resolve(undefined) + + else + resolve(res) + }), + ) + assert(response.status === 200) + assert(signInContextWorks && signInAPIContextWorks && createNewSessionContextWorks) + }) +}) diff --git a/test/useridmapping/createUserIdMapping.test.js b/test/useridmapping/createUserIdMapping.test.js deleted file mode 100644 index c465f6473..000000000 --- a/test/useridmapping/createUserIdMapping.test.js +++ /dev/null @@ -1,270 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword").default; -const SessionRecipe = require("../../lib/build/recipe/session").default; -const UserMetadataRecipe = require("../../lib/build/recipe/usermetadata").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`createUserIdMappingTest: ${printPath("[test/useridmapping/createUserIdMapping.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("createUserIdMappingTest", () => { - it("create a userId mapping", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalUserId = "externalId"; - let externalUserIdInfo = "externalIdInfo"; - - // create the userId mapping - let createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId, - externalUserIdInfo, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1); - assert.strictEqual(createUserIdMappingResponse.status, "OK"); - - // check that the userId mapping exists - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalUserIdInfo); - }); - - it("create a userId mapping with an unknown superTokensUserId", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create the userId mapping - let createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId: "unknownuUserId", - externalUserId: "externalId", - externalUserIdInfo: "externalInfo", - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1); - assert.strictEqual(createUserIdMappingResponse.status, "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"); - }); - - it("create a userId mapping when a mapping already exists", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a UserId mapping - - const signInResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signInResponse.status, "OK"); - - const superTokensUserId = signInResponse.user.id; - const externalId = "externalId"; - { - const createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1); - assert.strictEqual(createUserIdMappingResponse.status, "OK"); - } - - // create a duplicate mapping where both superTokensUserId and externalId already exist - { - const createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 3); - assert.strictEqual(createUserIdMappingResponse.status, "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"); - assert.strictEqual(createUserIdMappingResponse.doesSuperTokensUserIdExist, true); - assert.strictEqual(createUserIdMappingResponse.doesExternalUserIdExist, true); - } - - // create a duplicate mapping where both superTokensUserId already exists - { - const createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: "newExternalUserId", - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 3); - assert.strictEqual(createUserIdMappingResponse.status, "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"); - assert.strictEqual(createUserIdMappingResponse.doesSuperTokensUserIdExist, true); - assert.strictEqual(createUserIdMappingResponse.doesExternalUserIdExist, false); - } - - // create a duplicate mapping where both externalUserId already exists - { - const newUserSignInResponse = await EmailPasswordRecipe.signUp("testnew@example.com", "testPass123"); - assert.strictEqual(newUserSignInResponse.status, "OK"); - - const createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId: newUserSignInResponse.user.id, - externalUserId: externalId, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 3); - assert.strictEqual(createUserIdMappingResponse.status, "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"); - assert.strictEqual(createUserIdMappingResponse.doesSuperTokensUserIdExist, false); - assert.strictEqual(createUserIdMappingResponse.doesExternalUserIdExist, true); - } - }); - - it("create a userId mapping when userId already has usermetadata with and without force", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), UserMetadataRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - - const signInResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signInResponse.status, "OK"); - const superTokensUserId = signInResponse.user.id; - - // add metadata to the user - const testMetadata = { - role: "admin", - }; - await UserMetadataRecipe.updateUserMetadata(superTokensUserId, testMetadata); - - const externalId = "externalId"; - // without force - { - try { - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - throw new Error("Should not come here"); - } catch (error) { - assert(error.message.includes("UserId is already in use in UserMetadata recipe")); - } - } - // with force set to false - { - try { - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - force: false, - }); - throw new Error("Should not come here"); - } catch (error) { - assert(error.message.includes("UserId is already in use in UserMetadata recipe")); - } - } - // with force set to true - { - { - let response = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - force: true, - }); - assert.strictEqual(response.status, "OK"); - } - - // check that mapping exists - { - let response = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.superTokensUserId, superTokensUserId); - assert.strictEqual(response.externalUserId, externalId); - } - } - }); - }); -}); diff --git a/test/useridmapping/createUserIdMapping.test.ts b/test/useridmapping/createUserIdMapping.test.ts new file mode 100644 index 000000000..6f2202f08 --- /dev/null +++ b/test/useridmapping/createUserIdMapping.test.ts @@ -0,0 +1,268 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword' +import SessionRecipe from 'supertokens-node/recipe/session' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`createUserIdMappingTest: ${printPath('[test/useridmapping/createUserIdMapping.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('createUserIdMappingTest', () => { + it('create a userId mapping', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalUserId = 'externalId' + const externalUserIdInfo = 'externalIdInfo' + + // create the userId mapping + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId, + externalUserIdInfo, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1) + assert.strictEqual(createUserIdMappingResponse.status, 'OK') + + // check that the userId mapping exists + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalUserIdInfo) + }) + + it('create a userId mapping with an unknown superTokensUserId', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create the userId mapping + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId: 'unknownuUserId', + externalUserId: 'externalId', + externalUserIdInfo: 'externalInfo', + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1) + assert.strictEqual(createUserIdMappingResponse.status, 'UNKNOWN_SUPERTOKENS_USER_ID_ERROR') + }) + + it('create a userId mapping when a mapping already exists', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a UserId mapping + + const signInResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signInResponse.status, 'OK') + + const superTokensUserId = signInResponse.user.id + const externalId = 'externalId' + { + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1) + assert.strictEqual(createUserIdMappingResponse.status, 'OK') + } + + // create a duplicate mapping where both superTokensUserId and externalId already exist + { + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 3) + assert.strictEqual(createUserIdMappingResponse.status, 'USER_ID_MAPPING_ALREADY_EXISTS_ERROR') + assert.strictEqual(createUserIdMappingResponse.doesSuperTokensUserIdExist, true) + assert.strictEqual(createUserIdMappingResponse.doesExternalUserIdExist, true) + } + + // create a duplicate mapping where both superTokensUserId already exists + { + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: 'newExternalUserId', + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 3) + assert.strictEqual(createUserIdMappingResponse.status, 'USER_ID_MAPPING_ALREADY_EXISTS_ERROR') + assert.strictEqual(createUserIdMappingResponse.doesSuperTokensUserIdExist, true) + assert.strictEqual(createUserIdMappingResponse.doesExternalUserIdExist, false) + } + + // create a duplicate mapping where both externalUserId already exists + { + const newUserSignInResponse = await EmailPasswordRecipe.signUp('testnew@example.com', 'testPass123') + assert.strictEqual(newUserSignInResponse.status, 'OK') + + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId: newUserSignInResponse.user.id, + externalUserId: externalId, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 3) + assert.strictEqual(createUserIdMappingResponse.status, 'USER_ID_MAPPING_ALREADY_EXISTS_ERROR') + assert.strictEqual(createUserIdMappingResponse.doesSuperTokensUserIdExist, false) + assert.strictEqual(createUserIdMappingResponse.doesExternalUserIdExist, true) + } + }) + + it('create a userId mapping when userId already has usermetadata with and without force', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), UserMetadataRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + + const signInResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signInResponse.status, 'OK') + const superTokensUserId = signInResponse.user.id + + // add metadata to the user + const testMetadata = { + role: 'admin', + } + await UserMetadataRecipe.updateUserMetadata(superTokensUserId, testMetadata) + + const externalId = 'externalId' + // without force + { + try { + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + throw new Error('Should not come here') + } + catch (error) { + assert(error.message.includes('UserId is already in use in UserMetadata recipe')) + } + } + // with force set to false + { + try { + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + force: false, + }) + throw new Error('Should not come here') + } + catch (error) { + assert(error.message.includes('UserId is already in use in UserMetadata recipe')) + } + } + // with force set to true + { + { + const response = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + force: true, + }) + assert.strictEqual(response.status, 'OK') + } + + // check that mapping exists + { + const response = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.superTokensUserId, superTokensUserId) + assert.strictEqual(response.externalUserId, externalId) + } + } + }) + }) +}) diff --git a/test/useridmapping/deleteUserIdMapping.test.js b/test/useridmapping/deleteUserIdMapping.test.js deleted file mode 100644 index 8e069194b..000000000 --- a/test/useridmapping/deleteUserIdMapping.test.js +++ /dev/null @@ -1,355 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword").default; -const SessionRecipe = require("../../lib/build/recipe/session").default; -const UserMetadataRecipe = require("../../lib/build/recipe/usermetadata").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`deleteUserIdMappingTest: ${printPath("[test/useridmapping/deleteUserIdMapping.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("deleteUserIdMapping:", () => { - it("delete an unknown userId mapping", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - { - let response = await STExpress.deleteUserIdMapping({ userId: "unknown", userIdType: "SUPERTOKENS" }); - assert.strictEqual(Object.keys(response).length, 2); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.didMappingExist, false); - } - - { - let response = await STExpress.deleteUserIdMapping({ userId: "unknown", userIdType: "EXTERNAL" }); - assert.strictEqual(Object.keys(response).length, 2); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.didMappingExist, false); - } - - { - let response = await STExpress.deleteUserIdMapping({ userId: "unknown", userIdType: "ANY" }); - assert.strictEqual(Object.keys(response).length, 2); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.didMappingExist, false); - } - }); - - it("delete a userId mapping with userIdType as SUPERTOKENS", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - let externalIdInfo = "externalIdInfo"; - - // create the userId mapping - await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo); - - // delete the mapping - let deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - - assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2); - assert.strictEqual(deleteUserIdMappingResponse.status, "OK"); - assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true); - - // check that the mapping is deleted - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1); - assert.strictEqual(getUserIdMappingResponse.status, "UNKNOWN_MAPPING_ERROR"); - } - }); - - it("delete a userId mapping with userIdType as EXTERNAL", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - let externalUserIdInfo = "externalIdInfo"; - - // create the userId mapping - await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalUserIdInfo); - - // delete the mapping - let deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - }); - - assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2); - assert.strictEqual(deleteUserIdMappingResponse.status, "OK"); - assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true); - - // check that the mapping is deleted - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1); - assert.strictEqual(getUserIdMappingResponse.status, "UNKNOWN_MAPPING_ERROR"); - } - }); - - it("delete a userId mapping with userIdType as ANY", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - let externalIdInfo = "externalIdInfo"; - - // create the userId mapping - { - await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo); - - // delete the mapping with the supertokensUserId and ANY - let deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ - userId: superTokensUserId, - userIdType: "ANY", - }); - - assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2); - assert.strictEqual(deleteUserIdMappingResponse.status, "OK"); - assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true); - } - - // check that the mapping is deleted - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "ANY", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1); - assert.strictEqual(getUserIdMappingResponse.status, "UNKNOWN_MAPPING_ERROR"); - } - - // create the mapping - await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo); - - // delete the mapping with externalId and ANY - { - let deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ - userId: externalId, - userIdType: "ANY", - }); - - assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2); - assert.strictEqual(deleteUserIdMappingResponse.status, "OK"); - assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true); - } - - // check that the mapping is deleted - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - userIdType: "ANY", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1); - assert.strictEqual(getUserIdMappingResponse.status, "UNKNOWN_MAPPING_ERROR"); - } - }); - - it("delete a userId mapping when userMetadata exists with externalId with and without force", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), UserMetadataRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user and map their userId - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - let externalIdInfo = "test"; - - await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo); - - // add metadata to the user - const testMetadata = { - role: "admin", - }; - await UserMetadataRecipe.updateUserMetadata(externalId, testMetadata); - - // delete UserIdMapping without passing force - { - try { - await STExpress.deleteUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - }); - throw new Error("Should not come here"); - } catch (error) { - assert(error.message.includes("UserId is already in use in UserMetadata recipe")); - } - } - - // try deleting mapping with force set to false - { - try { - await STExpress.deleteUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - force: false, - }); - throw new Error("Should not come here"); - } catch (error) { - assert(error.message.includes("UserId is already in use in UserMetadata recipe")); - } - } - - // delete mapping with force set to true - { - let deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - force: true, - }); - assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2); - assert.strictEqual(deleteUserIdMappingResponse.status, "OK"); - assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true); - } - }); - }); - - async function createUserIdMappingAndCheckThatItExists(superTokensUserId, externalUserId, externalUserIdInfo) { - { - let response = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId, - externalUserIdInfo, - }); - assert.strictEqual(response.status, "OK"); - } - - { - let response = await STExpress.getUserIdMapping({ userId: superTokensUserId, userIdType: "SUPERTOKENS" }); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.superTokensUserId, superTokensUserId); - assert.strictEqual(response.externalUserId, externalUserId); - assert.strictEqual(response.externalUserIdInfo, externalUserIdInfo); - } - } -}); diff --git a/test/useridmapping/deleteUserIdMapping.test.ts b/test/useridmapping/deleteUserIdMapping.test.ts new file mode 100644 index 000000000..a17da656e --- /dev/null +++ b/test/useridmapping/deleteUserIdMapping.test.ts @@ -0,0 +1,352 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword' +import SessionRecipe from 'supertokens-node/recipe/session' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`deleteUserIdMappingTest: ${printPath('[test/useridmapping/deleteUserIdMapping.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('deleteUserIdMapping:', () => { + it('delete an unknown userId mapping', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + { + const response = await STExpress.deleteUserIdMapping({ userId: 'unknown', userIdType: 'SUPERTOKENS' }) + assert.strictEqual(Object.keys(response).length, 2) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.didMappingExist, false) + } + + { + const response = await STExpress.deleteUserIdMapping({ userId: 'unknown', userIdType: 'EXTERNAL' }) + assert.strictEqual(Object.keys(response).length, 2) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.didMappingExist, false) + } + + { + const response = await STExpress.deleteUserIdMapping({ userId: 'unknown', userIdType: 'ANY' }) + assert.strictEqual(Object.keys(response).length, 2) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.didMappingExist, false) + } + }) + + it('delete a userId mapping with userIdType as SUPERTOKENS', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + const externalIdInfo = 'externalIdInfo' + + // create the userId mapping + await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo) + + // delete the mapping + const deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + + assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2) + assert.strictEqual(deleteUserIdMappingResponse.status, 'OK') + assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true) + + // check that the mapping is deleted + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1) + assert.strictEqual(getUserIdMappingResponse.status, 'UNKNOWN_MAPPING_ERROR') + } + }) + + it('delete a userId mapping with userIdType as EXTERNAL', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + const externalUserIdInfo = 'externalIdInfo' + + // create the userId mapping + await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalUserIdInfo) + + // delete the mapping + const deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + }) + + assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2) + assert.strictEqual(deleteUserIdMappingResponse.status, 'OK') + assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true) + + // check that the mapping is deleted + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1) + assert.strictEqual(getUserIdMappingResponse.status, 'UNKNOWN_MAPPING_ERROR') + } + }) + + it('delete a userId mapping with userIdType as ANY', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + const externalIdInfo = 'externalIdInfo' + + // create the userId mapping + { + await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo) + + // delete the mapping with the supertokensUserId and ANY + const deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ + userId: superTokensUserId, + userIdType: 'ANY', + }) + + assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2) + assert.strictEqual(deleteUserIdMappingResponse.status, 'OK') + assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true) + } + + // check that the mapping is deleted + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'ANY', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1) + assert.strictEqual(getUserIdMappingResponse.status, 'UNKNOWN_MAPPING_ERROR') + } + + // create the mapping + await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo) + + // delete the mapping with externalId and ANY + { + const deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ + userId: externalId, + userIdType: 'ANY', + }) + + assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2) + assert.strictEqual(deleteUserIdMappingResponse.status, 'OK') + assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true) + } + + // check that the mapping is deleted + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + userIdType: 'ANY', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1) + assert.strictEqual(getUserIdMappingResponse.status, 'UNKNOWN_MAPPING_ERROR') + } + }) + + it('delete a userId mapping when userMetadata exists with externalId with and without force', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), UserMetadataRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user and map their userId + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + const externalIdInfo = 'test' + + await createUserIdMappingAndCheckThatItExists(superTokensUserId, externalId, externalIdInfo) + + // add metadata to the user + const testMetadata = { + role: 'admin', + } + await UserMetadataRecipe.updateUserMetadata(externalId, testMetadata) + + // delete UserIdMapping without passing force + { + try { + await STExpress.deleteUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + }) + throw new Error('Should not come here') + } + catch (error) { + assert(error.message.includes('UserId is already in use in UserMetadata recipe')) + } + } + + // try deleting mapping with force set to false + { + try { + await STExpress.deleteUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + force: false, + }) + throw new Error('Should not come here') + } + catch (error) { + assert(error.message.includes('UserId is already in use in UserMetadata recipe')) + } + } + + // delete mapping with force set to true + { + const deleteUserIdMappingResponse = await STExpress.deleteUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + force: true, + }) + assert.strictEqual(Object.keys(deleteUserIdMappingResponse).length, 2) + assert.strictEqual(deleteUserIdMappingResponse.status, 'OK') + assert.strictEqual(deleteUserIdMappingResponse.didMappingExist, true) + } + }) + }) + + async function createUserIdMappingAndCheckThatItExists(superTokensUserId, externalUserId, externalUserIdInfo) { + { + const response = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId, + externalUserIdInfo, + }) + assert.strictEqual(response.status, 'OK') + } + + { + const response = await STExpress.getUserIdMapping({ userId: superTokensUserId, userIdType: 'SUPERTOKENS' }) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.superTokensUserId, superTokensUserId) + assert.strictEqual(response.externalUserId, externalUserId) + assert.strictEqual(response.externalUserIdInfo, externalUserIdInfo) + } + } +}) diff --git a/test/useridmapping/getUserIdMapping.test.js b/test/useridmapping/getUserIdMapping.test.js deleted file mode 100644 index b872abb6e..000000000 --- a/test/useridmapping/getUserIdMapping.test.js +++ /dev/null @@ -1,254 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword").default; -const SessionRecipe = require("../../lib/build/recipe/session").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`getUserIdMappingTest: ${printPath("[test/useridmapping/getUserIdMapping.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getUserIdMappingTest", () => { - it("get userId mapping", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - let externalIdInfo = "externalIdInfo"; - - // create the userId mapping - let createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - externalUserIdInfo: externalIdInfo, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1); - assert.strictEqual(createUserIdMappingResponse.status, "OK"); - - // check that the userId mapping exists with userIdType as SUPERTOKENS - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo); - } - - // check that userId mapping exists with userIdType as EXTERNAL - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - userIdType: "ANY", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo); - } - - // check that userId mapping exists without passing userIdType - { - // while using the superTokensUserId - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo); - } - - // while using the externalUserId - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo); - } - } - }); - - it("get userId mapping when mapping does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: "unknownId", - userIdType: "SUPERTOKENS", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1); - assert.strictEqual(getUserIdMappingResponse.status, "UNKNOWN_MAPPING_ERROR"); - } - - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: "unknownId", - userIdType: "EXTERNAL", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1); - assert.strictEqual(getUserIdMappingResponse.status, "UNKNOWN_MAPPING_ERROR"); - } - - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: "unknownId", - userIdType: "ANY", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1); - assert.strictEqual(getUserIdMappingResponse.status, "UNKNOWN_MAPPING_ERROR"); - } - }); - - it("get userId mapping when externalUserIdInfo does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - - // create the userId mapping - let createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1); - assert.strictEqual(createUserIdMappingResponse.status, "OK"); - - // with userIdType as SUPERTOKENS - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - } - - // with userIdType as EXTERNAL - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - } - - // without userIdType - { - // with supertokensUserId - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - } - - // with externalUserId - { - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - }); - assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3); - assert.strictEqual(getUserIdMappingResponse.status, "OK"); - assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId); - assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId); - } - } - }); - }); -}); diff --git a/test/useridmapping/getUserIdMapping.test.ts b/test/useridmapping/getUserIdMapping.test.ts new file mode 100644 index 000000000..382ad826d --- /dev/null +++ b/test/useridmapping/getUserIdMapping.test.ts @@ -0,0 +1,253 @@ +// const assert = require("assert"); + +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getUserIdMappingTest: ${printPath('[test/useridmapping/getUserIdMapping.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getUserIdMappingTest', () => { + it('get userId mapping', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + const externalIdInfo = 'externalIdInfo' + + // create the userId mapping + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + externalUserIdInfo: externalIdInfo, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1) + assert.strictEqual(createUserIdMappingResponse.status, 'OK') + + // check that the userId mapping exists with userIdType as SUPERTOKENS + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo) + } + + // check that userId mapping exists with userIdType as EXTERNAL + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + userIdType: 'ANY', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo) + } + + // check that userId mapping exists without passing userIdType + { + // while using the superTokensUserId + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo) + } + + // while using the externalUserId + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 4) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + assert.strictEqual(getUserIdMappingResponse.externalUserIdInfo, externalIdInfo) + } + } + }) + + it('get userId mapping when mapping does not exist', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: 'unknownId', + userIdType: 'SUPERTOKENS', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1) + assert.strictEqual(getUserIdMappingResponse.status, 'UNKNOWN_MAPPING_ERROR') + } + + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: 'unknownId', + userIdType: 'EXTERNAL', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1) + assert.strictEqual(getUserIdMappingResponse.status, 'UNKNOWN_MAPPING_ERROR') + } + + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: 'unknownId', + userIdType: 'ANY', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 1) + assert.strictEqual(getUserIdMappingResponse.status, 'UNKNOWN_MAPPING_ERROR') + } + }) + + it('get userId mapping when externalUserIdInfo does not exist', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + + // create the userId mapping + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1) + assert.strictEqual(createUserIdMappingResponse.status, 'OK') + + // with userIdType as SUPERTOKENS + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + } + + // with userIdType as EXTERNAL + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + } + + // without userIdType + { + // with supertokensUserId + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + } + + // with externalUserId + { + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + }) + assert.strictEqual(Object.keys(getUserIdMappingResponse).length, 3) + assert.strictEqual(getUserIdMappingResponse.status, 'OK') + assert.strictEqual(getUserIdMappingResponse.superTokensUserId, superTokensUserId) + assert.strictEqual(getUserIdMappingResponse.externalUserId, externalId) + } + } + }) + }) +}) diff --git a/test/useridmapping/recipeTests/emailpassword.test.js b/test/useridmapping/recipeTests/emailpassword.test.js deleted file mode 100644 index 07261d8f9..000000000 --- a/test/useridmapping/recipeTests/emailpassword.test.js +++ /dev/null @@ -1,340 +0,0 @@ -const assert = require("assert"); -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -const { ProcessState } = require("../../../lib/build/processState"); -const STExpress = require("../../.."); -const EmailPasswordRecipe = require("../../../lib/build/recipe/emailpassword").default; -const SessionRecipe = require("../../../lib/build/recipe/session").default; -const { Querier } = require("../../../lib/build/querier"); -const { maxVersion } = require("../../../lib/build/utils"); - -describe(`userIdMapping with emailpassword: ${printPath( - "[test/useridmapping/recipeTests/emailpassword.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getUserById", () => { - it("create an emailPassword user and map their userId, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a new EmailPassword User - const email = "test@example.com"; - const password = "testPass123"; - - let signUpResponse = await EmailPasswordRecipe.signUp(email, password); - assert.strictEqual(signUpResponse.status, "OK"); - let user = signUpResponse.user; - let superTokensUserId = user.id; - - // retrieve the users info, the id should be the superTokens userId - { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); - assert.strictEqual(response.id, superTokensUserId); - } - - let externalId = "externalId"; - - // map the users id - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the users info using the superTokensUserId, the id in the response should be the externalId - { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); - } - - // retrieve the users info using the externalId, the id in the response should be the externalId - { - let response = await EmailPasswordRecipe.getUserById(externalId); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); - } - }); - }); - - describe("getUserByEmail", () => { - it("create an emailPassword user and map their userId, retrieve the user info using getUserByEmail and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a new EmailPassword User - const email = "test@example.com"; - const password = "testPass123"; - - let signUpResponse = await EmailPasswordRecipe.signUp(email, password); - assert.strictEqual(signUpResponse.status, "OK"); - let user = signUpResponse.user; - let superTokensUserId = user.id; - - // retrieve the users info, the id should be the superTokens userId - { - let response = await EmailPasswordRecipe.getUserByEmail(email); - assert.strictEqual(response.id, superTokensUserId); - } - - let externalId = "externalId"; - - // map the users id - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the users info using email, the id in the response should be the externalId - { - let response = await EmailPasswordRecipe.getUserByEmail(email); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); - } - }); - }); - - describe("signIn", () => { - it("create an emailPassword user and map their userId, signIn, check that the userRetrieved has the mapped userId", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a new EmailPassword User - const email = "test@example.com"; - const password = "testPass123"; - - let signUpResponse = await EmailPasswordRecipe.signUp(email, password); - assert.strictEqual(signUpResponse.status, "OK"); - let user = signUpResponse.user; - let superTokensUserId = user.id; - - // retrieve the users info, the id should be the superTokens userId - { - let response = await EmailPasswordRecipe.getUserByEmail(email); - assert.strictEqual(response.id, superTokensUserId); - } - - let externalId = "externalId"; - - // map the users id - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // sign in, check that the userId retrieved is the external userId - let signInResponse = await EmailPasswordRecipe.signIn(email, password); - assert.strictEqual(signInResponse.status, "OK"); - assert.strictEqual(signInResponse.user.id, externalId); - }); - }); - - describe("password reset", () => { - it("create an emailPassword user and map their userId, and do a password reset using the external id, check that it gets reset", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a new EmailPassword User - const email = "test@example.com"; - const password = "testPass123"; - - let signUpResponse = await EmailPasswordRecipe.signUp(email, password); - assert.strictEqual(signUpResponse.status, "OK"); - let user = signUpResponse.user; - let superTokensUserId = user.id; - - // retrieve the users info, the id should be the superTokens userId - { - let response = await EmailPasswordRecipe.getUserByEmail(email); - assert.strictEqual(response.id, superTokensUserId); - } - - // map the userId - const externalId = "externalId"; - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - // create the password resestToken - let createResetPasswordTokenResponse = await EmailPasswordRecipe.createResetPasswordToken(externalId); - assert.strictEqual(createResetPasswordTokenResponse.status, "OK"); - - // reset the password - const newPassword = "newTestPass123"; - let resetPasswordUsingTokenResponse = await EmailPasswordRecipe.resetPasswordUsingToken( - createResetPasswordTokenResponse.token, - newPassword - ); - assert.strictEqual(resetPasswordUsingTokenResponse.status, "OK"); - assert.strictEqual(resetPasswordUsingTokenResponse.userId, externalId); - - // check that the password is reset by signing in - let response = await EmailPasswordRecipe.signIn(email, newPassword); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.user.id, externalId); - }); - }); - - describe("update email and password", () => { - it("create an emailPassword user and map their userId, update their email and password using the externalId", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a new EmailPassword User - const email = "test@example.com"; - const password = "testPass123"; - - let signUpResponse = await EmailPasswordRecipe.signUp(email, password); - assert.strictEqual(signUpResponse.status, "OK"); - let user = signUpResponse.user; - let superTokensUserId = user.id; - - // retrieve the users info, the id should be the superTokens userId - { - let response = await EmailPasswordRecipe.getUserByEmail(email); - assert.strictEqual(response.id, superTokensUserId); - } - - // map the userId - const externalId = "externalId"; - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // update the email using the externalId - const updatedEmail = "test123@example.com"; - { - { - const response = await EmailPasswordRecipe.updateEmailOrPassword({ - userId: externalId, - email: updatedEmail, - }); - assert.strictEqual(response.status, "OK"); - } - - // sign in with the new email - { - const response = await EmailPasswordRecipe.signIn(updatedEmail, password); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.user.id, externalId); - } - } - - // update the password using the externalId - const updatedPassword = "newTestPass123"; - { - { - const response = await EmailPasswordRecipe.updateEmailOrPassword({ - userId: externalId, - password: updatedPassword, - }); - assert.strictEqual(response.status, "OK"); - } - - // sign in with new password - { - const response = await EmailPasswordRecipe.signIn(updatedEmail, updatedPassword); - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.user.id, externalId); - } - } - }); - }); -}); diff --git a/test/useridmapping/recipeTests/emailpassword.test.ts b/test/useridmapping/recipeTests/emailpassword.test.ts new file mode 100644 index 000000000..355d40d04 --- /dev/null +++ b/test/useridmapping/recipeTests/emailpassword.test.ts @@ -0,0 +1,336 @@ +import assert from 'assert' +import { afterAll, beforeEach, describe, it } from 'vitest' +import STExpress from 'supertokens-node' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { ProcessState } from 'supertokens-node/processState' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +describe(`userIdMapping with emailpassword: ${printPath( + '[test/useridmapping/recipeTests/emailpassword.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getUserById', () => { + it('create an emailPassword user and map their userId, retrieve the user info using getUserById and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a new EmailPassword User + const email = 'test@example.com' + const password = 'testPass123' + + const signUpResponse = await EmailPasswordRecipe.signUp(email, password) + assert.strictEqual(signUpResponse.status, 'OK') + const user = signUpResponse.user + const superTokensUserId = user.id + + // retrieve the users info, the id should be the superTokens userId + { + const response = await EmailPasswordRecipe.getUserById(superTokensUserId) + assert.strictEqual(response.id, superTokensUserId) + } + + const externalId = 'externalId' + + // map the users id + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the users info using the superTokensUserId, the id in the response should be the externalId + { + const response = await EmailPasswordRecipe.getUserById(superTokensUserId) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.email, email) + } + + // retrieve the users info using the externalId, the id in the response should be the externalId + { + const response = await EmailPasswordRecipe.getUserById(externalId) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.email, email) + } + }) + }) + + describe('getUserByEmail', () => { + it('create an emailPassword user and map their userId, retrieve the user info using getUserByEmail and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a new EmailPassword User + const email = 'test@example.com' + const password = 'testPass123' + + const signUpResponse = await EmailPasswordRecipe.signUp(email, password) + assert.strictEqual(signUpResponse.status, 'OK') + const user = signUpResponse.user + const superTokensUserId = user.id + + // retrieve the users info, the id should be the superTokens userId + { + const response = await EmailPasswordRecipe.getUserByEmail(email) + assert.strictEqual(response.id, superTokensUserId) + } + + const externalId = 'externalId' + + // map the users id + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the users info using email, the id in the response should be the externalId + { + const response = await EmailPasswordRecipe.getUserByEmail(email) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.email, email) + } + }) + }) + + describe('signIn', () => { + it('create an emailPassword user and map their userId, signIn, check that the userRetrieved has the mapped userId', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a new EmailPassword User + const email = 'test@example.com' + const password = 'testPass123' + + const signUpResponse = await EmailPasswordRecipe.signUp(email, password) + assert.strictEqual(signUpResponse.status, 'OK') + const user = signUpResponse.user + const superTokensUserId = user.id + + // retrieve the users info, the id should be the superTokens userId + { + const response = await EmailPasswordRecipe.getUserByEmail(email) + assert.strictEqual(response.id, superTokensUserId) + } + + const externalId = 'externalId' + + // map the users id + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // sign in, check that the userId retrieved is the external userId + const signInResponse = await EmailPasswordRecipe.signIn(email, password) + assert.strictEqual(signInResponse.status, 'OK') + assert.strictEqual(signInResponse.user.id, externalId) + }) + }) + + describe('password reset', () => { + it('create an emailPassword user and map their userId, and do a password reset using the external id, check that it gets reset', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a new EmailPassword User + const email = 'test@example.com' + const password = 'testPass123' + + const signUpResponse = await EmailPasswordRecipe.signUp(email, password) + assert.strictEqual(signUpResponse.status, 'OK') + const user = signUpResponse.user + const superTokensUserId = user.id + + // retrieve the users info, the id should be the superTokens userId + { + const response = await EmailPasswordRecipe.getUserByEmail(email) + assert.strictEqual(response.id, superTokensUserId) + } + + // map the userId + const externalId = 'externalId' + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + // create the password resestToken + const createResetPasswordTokenResponse = await EmailPasswordRecipe.createResetPasswordToken(externalId) + assert.strictEqual(createResetPasswordTokenResponse.status, 'OK') + + // reset the password + const newPassword = 'newTestPass123' + const resetPasswordUsingTokenResponse = await EmailPasswordRecipe.resetPasswordUsingToken( + createResetPasswordTokenResponse.token, + newPassword, + ) + assert.strictEqual(resetPasswordUsingTokenResponse.status, 'OK') + assert.strictEqual(resetPasswordUsingTokenResponse.userId, externalId) + + // check that the password is reset by signing in + const response = await EmailPasswordRecipe.signIn(email, newPassword) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.user.id, externalId) + }) + }) + + describe('update email and password', () => { + it('create an emailPassword user and map their userId, update their email and password using the externalId', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a new EmailPassword User + const email = 'test@example.com' + const password = 'testPass123' + + const signUpResponse = await EmailPasswordRecipe.signUp(email, password) + assert.strictEqual(signUpResponse.status, 'OK') + const user = signUpResponse.user + const superTokensUserId = user.id + + // retrieve the users info, the id should be the superTokens userId + { + const response = await EmailPasswordRecipe.getUserByEmail(email) + assert.strictEqual(response.id, superTokensUserId) + } + + // map the userId + const externalId = 'externalId' + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // update the email using the externalId + const updatedEmail = 'test123@example.com' + { + { + const response = await EmailPasswordRecipe.updateEmailOrPassword({ + userId: externalId, + email: updatedEmail, + }) + assert.strictEqual(response.status, 'OK') + } + + // sign in with the new email + { + const response = await EmailPasswordRecipe.signIn(updatedEmail, password) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.user.id, externalId) + } + } + + // update the password using the externalId + const updatedPassword = 'newTestPass123' + { + { + const response = await EmailPasswordRecipe.updateEmailOrPassword({ + userId: externalId, + password: updatedPassword, + }) + assert.strictEqual(response.status, 'OK') + } + + // sign in with new password + { + const response = await EmailPasswordRecipe.signIn(updatedEmail, updatedPassword) + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.user.id, externalId) + } + } + }) + }) +}) diff --git a/test/useridmapping/recipeTests/passwordless.test.js b/test/useridmapping/recipeTests/passwordless.test.js deleted file mode 100644 index 26c15b6d3..000000000 --- a/test/useridmapping/recipeTests/passwordless.test.js +++ /dev/null @@ -1,377 +0,0 @@ -const assert = require("assert"); -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -const { ProcessState } = require("../../../lib/build/processState"); -const STExpress = require("../../.."); -const PasswordlessRecipe = require("../../../lib/build/recipe/passwordless").default; -const SessionRecipe = require("../../../lib/build/recipe/session").default; -const { Querier } = require("../../../lib/build/querier"); -const { maxVersion } = require("../../../lib/build/utils"); - -describe(`userIdMapping with passwordless: ${printPath( - "[test/useridmapping/recipeTests/passwordless.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("consumeCode", () => { - it("create a passwordless user and map their userId, signIn again and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - PasswordlessRecipe.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a Passwordless user - const email = "test@example.com"; - const codeInfo = await PasswordlessRecipe.createCode({ - email, - }); - - assert.strictEqual(codeInfo.status, "OK"); - - const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert.strictEqual(consumeCodeResponse.status, "OK"); - - const superTokensUserId = consumeCodeResponse.user.id; - const externalId = "externalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // sign in again and check and the externalId is returned - const codeInfo_2 = await PasswordlessRecipe.createCode({ - email, - }); - - assert.strictEqual(codeInfo_2.status, "OK"); - - const consumeCodeResponse_2 = await PasswordlessRecipe.consumeCode({ - preAuthSessionId: codeInfo_2.preAuthSessionId, - userInputCode: codeInfo_2.userInputCode, - deviceId: codeInfo_2.deviceId, - }); - - assert.strictEqual(consumeCodeResponse_2.status, "OK"); - assert.strictEqual(consumeCodeResponse_2.user.id, externalId); - }); - }); - - describe("getUserById", () => { - it("create a passwordless user and map their userId, call getUserById and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - PasswordlessRecipe.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a Passwordless user - const email = "test@example.com"; - const codeInfo = await PasswordlessRecipe.createCode({ - email, - }); - - assert.strictEqual(codeInfo.status, "OK"); - - const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert.strictEqual(consumeCodeResponse.status, "OK"); - - const superTokensUserId = consumeCodeResponse.user.id; - const externalId = "externalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - let response = await PasswordlessRecipe.getUserById({ - userId: externalId, - }); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - }); - }); - - describe("getUserByEmail", () => { - it("create a passwordless user and map their userId, call getUserByEmail and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - PasswordlessRecipe.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a Passwordless user - const email = "test@example.com"; - const codeInfo = await PasswordlessRecipe.createCode({ - email, - }); - - assert.strictEqual(codeInfo.status, "OK"); - - const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert.strictEqual(consumeCodeResponse.status, "OK"); - - const superTokensUserId = consumeCodeResponse.user.id; - const externalId = "externalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - let response = await PasswordlessRecipe.getUserByEmail({ - email, - }); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - }); - }); - - describe("getUserByPhoneNumber", () => { - it("create a passwordless user and map their userId, call getUserByPhoneNumber and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - PasswordlessRecipe.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a Passwordless user - const phoneNumber = "+911234566789"; - const codeInfo = await PasswordlessRecipe.createCode({ - phoneNumber, - }); - - assert.strictEqual(codeInfo.status, "OK"); - - const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert.strictEqual(consumeCodeResponse.status, "OK"); - - const superTokensUserId = consumeCodeResponse.user.id; - const externalId = "externalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - let response = await PasswordlessRecipe.getUserByPhoneNumber({ - phoneNumber, - }); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - }); - }); - - describe("updateUser", () => { - it("create a passwordless user and map their userId, call updateUser to add their email and retrieve the user to see if the changes are reflected", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - PasswordlessRecipe.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a Passwordless user - const phoneNumber = "+911234566789"; - const codeInfo = await PasswordlessRecipe.createCode({ - phoneNumber, - }); - - assert.strictEqual(codeInfo.status, "OK"); - - const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert.strictEqual(consumeCodeResponse.status, "OK"); - - const superTokensUserId = consumeCodeResponse.user.id; - const externalId = "externalId"; - const email = "test@example.com"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - let updateUserResponse = await PasswordlessRecipe.updateUser({ - userId: externalId, - email, - }); - assert.strictEqual(updateUserResponse.status, "OK"); - - // retrieve user - let response = await PasswordlessRecipe.getUserByPhoneNumber({ - phoneNumber, - }); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.phoneNumber, phoneNumber); - assert.strictEqual(response.email, email); - }); - }); -}); diff --git a/test/useridmapping/recipeTests/passwordless.test.ts b/test/useridmapping/recipeTests/passwordless.test.ts new file mode 100644 index 000000000..90233287c --- /dev/null +++ b/test/useridmapping/recipeTests/passwordless.test.ts @@ -0,0 +1,373 @@ +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import PasswordlessRecipe from 'supertokens-node/recipe/passwordless' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +describe(`userIdMapping with passwordless: ${printPath( + '[test/useridmapping/recipeTests/passwordless.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('consumeCode', () => { + it('create a passwordless user and map their userId, signIn again and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + PasswordlessRecipe.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a Passwordless user + const email = 'test@example.com' + const codeInfo = await PasswordlessRecipe.createCode({ + email, + }) + + assert.strictEqual(codeInfo.status, 'OK') + + const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert.strictEqual(consumeCodeResponse.status, 'OK') + + const superTokensUserId = consumeCodeResponse.user.id + const externalId = 'externalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // sign in again and check and the externalId is returned + const codeInfo_2 = await PasswordlessRecipe.createCode({ + email, + }) + + assert.strictEqual(codeInfo_2.status, 'OK') + + const consumeCodeResponse_2 = await PasswordlessRecipe.consumeCode({ + preAuthSessionId: codeInfo_2.preAuthSessionId, + userInputCode: codeInfo_2.userInputCode, + deviceId: codeInfo_2.deviceId, + }) + + assert.strictEqual(consumeCodeResponse_2.status, 'OK') + assert.strictEqual(consumeCodeResponse_2.user.id, externalId) + }) + }) + + describe('getUserById', () => { + it('create a passwordless user and map their userId, call getUserById and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + PasswordlessRecipe.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a Passwordless user + const email = 'test@example.com' + const codeInfo = await PasswordlessRecipe.createCode({ + email, + }) + + assert.strictEqual(codeInfo.status, 'OK') + + const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert.strictEqual(consumeCodeResponse.status, 'OK') + + const superTokensUserId = consumeCodeResponse.user.id + const externalId = 'externalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + const response = await PasswordlessRecipe.getUserById({ + userId: externalId, + }) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + }) + }) + + describe('getUserByEmail', () => { + it('create a passwordless user and map their userId, call getUserByEmail and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + PasswordlessRecipe.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a Passwordless user + const email = 'test@example.com' + const codeInfo = await PasswordlessRecipe.createCode({ + email, + }) + + assert.strictEqual(codeInfo.status, 'OK') + + const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert.strictEqual(consumeCodeResponse.status, 'OK') + + const superTokensUserId = consumeCodeResponse.user.id + const externalId = 'externalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + const response = await PasswordlessRecipe.getUserByEmail({ + email, + }) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + }) + }) + + describe('getUserByPhoneNumber', () => { + it('create a passwordless user and map their userId, call getUserByPhoneNumber and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + PasswordlessRecipe.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a Passwordless user + const phoneNumber = '+911234566789' + const codeInfo = await PasswordlessRecipe.createCode({ + phoneNumber, + }) + + assert.strictEqual(codeInfo.status, 'OK') + + const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert.strictEqual(consumeCodeResponse.status, 'OK') + + const superTokensUserId = consumeCodeResponse.user.id + const externalId = 'externalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + const response = await PasswordlessRecipe.getUserByPhoneNumber({ + phoneNumber, + }) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + }) + }) + + describe('updateUser', () => { + it('create a passwordless user and map their userId, call updateUser to add their email and retrieve the user to see if the changes are reflected', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + PasswordlessRecipe.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a Passwordless user + const phoneNumber = '+911234566789' + const codeInfo = await PasswordlessRecipe.createCode({ + phoneNumber, + }) + + assert.strictEqual(codeInfo.status, 'OK') + + const consumeCodeResponse = await PasswordlessRecipe.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert.strictEqual(consumeCodeResponse.status, 'OK') + + const superTokensUserId = consumeCodeResponse.user.id + const externalId = 'externalId' + const email = 'test@example.com' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + const updateUserResponse = await PasswordlessRecipe.updateUser({ + userId: externalId, + email, + }) + assert.strictEqual(updateUserResponse.status, 'OK') + + // retrieve user + const response = await PasswordlessRecipe.getUserByPhoneNumber({ + phoneNumber, + }) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.phoneNumber, phoneNumber) + assert.strictEqual(response.email, email) + }) + }) +}) diff --git a/test/useridmapping/recipeTests/supertokens.test.js b/test/useridmapping/recipeTests/supertokens.test.js deleted file mode 100644 index 3ab890fc5..000000000 --- a/test/useridmapping/recipeTests/supertokens.test.js +++ /dev/null @@ -1,172 +0,0 @@ -const assert = require("assert"); -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -const { ProcessState } = require("../../../lib/build/processState"); -const STExpress = require("../../.."); -const EmailPasswordRecipe = require("../../../lib/build/recipe/emailpassword").default; -const UserMetadataRecipe = require("../../../lib/build/recipe/usermetadata").default; -const SessionRecipe = require("../../../lib/build/recipe/session").default; -const { Querier } = require("../../../lib/build/querier"); -const { maxVersion } = require("../../../lib/build/utils"); - -describe(`userIdMapping with supertokens recipe: ${printPath( - "[test/useridmapping/recipeTests/supertokens.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("deleteUser", () => { - it("create an emailPassword user and map their userId, then delete user with the externalId", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), UserMetadataRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a new EmailPassword User - const email = "test@example.com"; - const password = "testPass123"; - - let signUpResponse = await EmailPasswordRecipe.signUp(email, password); - assert.strictEqual(signUpResponse.status, "OK"); - let user = signUpResponse.user; - let superTokensUserId = user.id; - - // retrieve the users info, the id should be the superTokens userId - { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); - assert.strictEqual(response.id, superTokensUserId); - } - - let externalId = "externalId"; - - // map the users id - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the users info using the superTokensUserId, the id in the response should be the externalId - { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); - } - - // add userMetadata to the user mapped with the externalId - { - const testMetadata = { - role: "admin", - }; - await UserMetadataRecipe.updateUserMetadata(externalId, testMetadata); - - // retrieve UserMetadata and check that it exists - let response = await UserMetadataRecipe.getUserMetadata(externalId); - assert.strictEqual(response.status, "OK"); - assert.deepStrictEqual(response.metadata, testMetadata); - } - - { - const response = await STExpress.deleteUser(externalId); - assert.strictEqual(response.status, "OK"); - } - - // check that user does not exist - { - let response = await EmailPasswordRecipe.getUserById(superTokensUserId); - assert.ok(response === undefined); - } - // check that no metadata exists for the id - { - let response = await UserMetadataRecipe.getUserMetadata(externalId); - assert.strictEqual(Object.keys(response.metadata).length, 0); - } - }); - }); - - describe("getUsers", () => { - it("create multiple users and map one of the users userId, retrieve all users and check that response will contain the externalId for the mapped user", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create multiple users - const email = ["test@example.com", "test1@example.com", "test2@example.com", "test3@example.com"]; - const password = "testPass123"; - let users = []; - - for (let i = 0; i < email.length; i++) { - let signUpResponse = await EmailPasswordRecipe.signUp(email[i], password); - assert.strictEqual(signUpResponse.status, "OK"); - users.push(signUpResponse.user); - } - - // the first users userId - const superTokensUserId = users[0].id; - - let externalId = "externalId"; - - // map the users id - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve all the users using getUsersNewestFirst - { - let response = await STExpress.getUsersNewestFirst(); - assert.strictEqual(response.users.length, 4); - // since the first user we created has their userId mapped we access the last element from the users array in the response - const oldestUsersId = response.users[response.users.length - 1].user.id; - assert.strictEqual(oldestUsersId, externalId); - } - - // retrieve all the users using getUsersOldestFirst - { - let response = await STExpress.getUsersOldestFirst(); - assert.strictEqual(response.users.length, 4); - - const oldestUsersId = response.users[0].user.id; - assert.strictEqual(oldestUsersId, externalId); - } - }); - }); -}); diff --git a/test/useridmapping/recipeTests/supertokens.test.ts b/test/useridmapping/recipeTests/supertokens.test.ts new file mode 100644 index 000000000..808036d8c --- /dev/null +++ b/test/useridmapping/recipeTests/supertokens.test.ts @@ -0,0 +1,171 @@ +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +describe(`userIdMapping with supertokens recipe: ${printPath( + '[test/useridmapping/recipeTests/supertokens.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('deleteUser', () => { + it('create an emailPassword user and map their userId, then delete user with the externalId', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), UserMetadataRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a new EmailPassword User + const email = 'test@example.com' + const password = 'testPass123' + + const signUpResponse = await EmailPasswordRecipe.signUp(email, password) + assert.strictEqual(signUpResponse.status, 'OK') + const user = signUpResponse.user + const superTokensUserId = user.id + + // retrieve the users info, the id should be the superTokens userId + { + const response = await EmailPasswordRecipe.getUserById(superTokensUserId) + assert.strictEqual(response.id, superTokensUserId) + } + + const externalId = 'externalId' + + // map the users id + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the users info using the superTokensUserId, the id in the response should be the externalId + { + const response = await EmailPasswordRecipe.getUserById(superTokensUserId) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.email, email) + } + + // add userMetadata to the user mapped with the externalId + { + const testMetadata = { + role: 'admin', + } + await UserMetadataRecipe.updateUserMetadata(externalId, testMetadata) + + // retrieve UserMetadata and check that it exists + const response = await UserMetadataRecipe.getUserMetadata(externalId) + assert.strictEqual(response.status, 'OK') + assert.deepStrictEqual(response.metadata, testMetadata) + } + + { + const response = await STExpress.deleteUser(externalId) + assert.strictEqual(response.status, 'OK') + } + + // check that user does not exist + { + const response = await EmailPasswordRecipe.getUserById(superTokensUserId) + assert.ok(response === undefined) + } + // check that no metadata exists for the id + { + const response = await UserMetadataRecipe.getUserMetadata(externalId) + assert.strictEqual(Object.keys(response.metadata).length, 0) + } + }) + }) + + describe('getUsers', () => { + it('create multiple users and map one of the users userId, retrieve all users and check that response will contain the externalId for the mapped user', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create multiple users + const email = ['test@example.com', 'test1@example.com', 'test2@example.com', 'test3@example.com'] + const password = 'testPass123' + const users = [] + + for (let i = 0; i < email.length; i++) { + const signUpResponse = await EmailPasswordRecipe.signUp(email[i], password) + assert.strictEqual(signUpResponse.status, 'OK') + users.push(signUpResponse.user) + } + + // the first users userId + const superTokensUserId = users[0].id + + const externalId = 'externalId' + + // map the users id + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve all the users using getUsersNewestFirst + { + const response = await STExpress.getUsersNewestFirst() + assert.strictEqual(response.users.length, 4) + // since the first user we created has their userId mapped we access the last element from the users array in the response + const oldestUsersId = response.users[response.users.length - 1].user.id + assert.strictEqual(oldestUsersId, externalId) + } + + // retrieve all the users using getUsersOldestFirst + { + const response = await STExpress.getUsersOldestFirst() + assert.strictEqual(response.users.length, 4) + + const oldestUsersId = response.users[0].user.id + assert.strictEqual(oldestUsersId, externalId) + } + }) + }) +}) diff --git a/test/useridmapping/recipeTests/thirdparty.test.js b/test/useridmapping/recipeTests/thirdparty.test.js deleted file mode 100644 index 0d74353f7..000000000 --- a/test/useridmapping/recipeTests/thirdparty.test.js +++ /dev/null @@ -1,242 +0,0 @@ -const assert = require("assert"); -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -const { ProcessState } = require("../../../lib/build/processState"); -const STExpress = require("../../.."); -const ThirdPartyRecipe = require("../../../lib/build/recipe/thirdparty").default; -const SessionRecipe = require("../../../lib/build/recipe/session").default; -const { Querier } = require("../../../lib/build/querier"); -const { maxVersion } = require("../../../lib/build/utils"); - -describe(`userIdMapping with thirdparty: ${printPath( - "[test/useridmapping/recipeTests/thirdparty.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("signInUp", () => { - it("create a thirdParty user and map their userId, signIn and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirdPartyRecipe.Google({ - clientId: "test", - clientSecret: "test", - }), - ], - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a thirdParty user - let signInUpResponse = await ThirdPartyRecipe.signInUp("google", "tpId", "test@example.com"); - - assert.strictEqual(signInUpResponse.status, "OK"); - const superTokensUserId = signInUpResponse.user.id; - const externalId = "externalId"; - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // sign in and check that the userId in the response is the externalId - let response = await ThirdPartyRecipe.signInUp("google", "tpId", "test@example.com"); - - assert.strictEqual(response.status, "OK"); - assert.strictEqual(response.createdNewUser, false); - assert.strictEqual(response.user.id, externalId); - }); - }); - - describe("getUserById", () => { - it("create a thirdParty user and map their userId, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirdPartyRecipe.Google({ - clientId: "test", - clientSecret: "test", - }), - ], - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a thirdParty user - let signInUpResponse = await ThirdPartyRecipe.signInUp("google", "tpId", "test@example.com"); - - assert.strictEqual(signInUpResponse.status, "OK"); - const superTokensUserId = signInUpResponse.user.id; - const externalId = "externalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the user - let response = await ThirdPartyRecipe.getUserById(externalId); - assert.ok(response != undefined); - assert.strictEqual(response.id, externalId); - }); - }); - - describe("getUsersByEmail", () => { - it("create a thirdParty user and map their userId, retrieve the user info using getUsersByEmail and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirdPartyRecipe.Google({ - clientId: "test", - clientSecret: "test", - }), - ], - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a thirdParty user - let signInUpResponse = await ThirdPartyRecipe.signInUp("google", "tpId", "test@example.com"); - - assert.strictEqual(signInUpResponse.status, "OK"); - const superTokensUserId = signInUpResponse.user.id; - const externalId = "externalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the user - let response = await ThirdPartyRecipe.getUsersByEmail("test@example.com"); - assert.strictEqual(response.length, 1); - assert.strictEqual(response[0].id, externalId); - }); - }); - - describe("getUserByThirdPartyInfo", () => { - it("create a thirdParty user and map their userId, retrieve the user info using getUserByThirdPartyInfo and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyRecipe.init({ - signInAndUpFeature: { - providers: [ - ThirdPartyRecipe.Google({ - clientId: "test", - clientSecret: "test", - }), - ], - }, - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a thirdParty user - const thirdPartyId = "google"; - const thirdPartyUserId = "tpId"; - let signInUpResponse = await ThirdPartyRecipe.signInUp(thirdPartyId, thirdPartyUserId, "test@example.com"); - - assert.strictEqual(signInUpResponse.status, "OK"); - const superTokensUserId = signInUpResponse.user.id; - const externalId = "externalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the user - let response = await ThirdPartyRecipe.getUserByThirdPartyInfo(thirdPartyId, thirdPartyUserId); - assert.ok(response != undefined); - assert.strictEqual(response.id, externalId); - }); - }); -}); diff --git a/test/useridmapping/recipeTests/thirdparty.test.ts b/test/useridmapping/recipeTests/thirdparty.test.ts new file mode 100644 index 000000000..90730c601 --- /dev/null +++ b/test/useridmapping/recipeTests/thirdparty.test.ts @@ -0,0 +1,239 @@ +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import ThirdPartyRecipe from 'supertokens-node/recipe/thirdparty' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +describe(`userIdMapping with thirdparty: ${printPath( + '[test/useridmapping/recipeTests/thirdparty.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('signInUp', () => { + it('create a thirdParty user and map their userId, signIn and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirdPartyRecipe.Google({ + clientId: 'test', + clientSecret: 'test', + }), + ], + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a thirdParty user + const signInUpResponse = await ThirdPartyRecipe.signInUp('google', 'tpId', 'test@example.com') + + assert.strictEqual(signInUpResponse.status, 'OK') + const superTokensUserId = signInUpResponse.user.id + const externalId = 'externalId' + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // sign in and check that the userId in the response is the externalId + const response = await ThirdPartyRecipe.signInUp('google', 'tpId', 'test@example.com') + + assert.strictEqual(response.status, 'OK') + assert.strictEqual(response.createdNewUser, false) + assert.strictEqual(response.user.id, externalId) + }) + }) + + describe('getUserById', () => { + it('create a thirdParty user and map their userId, retrieve the user info using getUserById and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirdPartyRecipe.Google({ + clientId: 'test', + clientSecret: 'test', + }), + ], + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a thirdParty user + const signInUpResponse = await ThirdPartyRecipe.signInUp('google', 'tpId', 'test@example.com') + + assert.strictEqual(signInUpResponse.status, 'OK') + const superTokensUserId = signInUpResponse.user.id + const externalId = 'externalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the user + const response = await ThirdPartyRecipe.getUserById(externalId) + assert.ok(response != undefined) + assert.strictEqual(response.id, externalId) + }) + }) + + describe('getUsersByEmail', () => { + it('create a thirdParty user and map their userId, retrieve the user info using getUsersByEmail and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirdPartyRecipe.Google({ + clientId: 'test', + clientSecret: 'test', + }), + ], + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a thirdParty user + const signInUpResponse = await ThirdPartyRecipe.signInUp('google', 'tpId', 'test@example.com') + + assert.strictEqual(signInUpResponse.status, 'OK') + const superTokensUserId = signInUpResponse.user.id + const externalId = 'externalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the user + const response = await ThirdPartyRecipe.getUsersByEmail('test@example.com') + assert.strictEqual(response.length, 1) + assert.strictEqual(response[0].id, externalId) + }) + }) + + describe('getUserByThirdPartyInfo', () => { + it('create a thirdParty user and map their userId, retrieve the user info using getUserByThirdPartyInfo and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyRecipe.init({ + signInAndUpFeature: { + providers: [ + ThirdPartyRecipe.Google({ + clientId: 'test', + clientSecret: 'test', + }), + ], + }, + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a thirdParty user + const thirdPartyId = 'google' + const thirdPartyUserId = 'tpId' + const signInUpResponse = await ThirdPartyRecipe.signInUp(thirdPartyId, thirdPartyUserId, 'test@example.com') + + assert.strictEqual(signInUpResponse.status, 'OK') + const superTokensUserId = signInUpResponse.user.id + const externalId = 'externalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the user + const response = await ThirdPartyRecipe.getUserByThirdPartyInfo(thirdPartyId, thirdPartyUserId) + assert.ok(response != undefined) + assert.strictEqual(response.id, externalId) + }) + }) +}) diff --git a/test/useridmapping/recipeTests/thirdpartyemailpassword.test.js b/test/useridmapping/recipeTests/thirdpartyemailpassword.test.js deleted file mode 100644 index 7eb82db53..000000000 --- a/test/useridmapping/recipeTests/thirdpartyemailpassword.test.js +++ /dev/null @@ -1,107 +0,0 @@ -const assert = require("assert"); -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -const { ProcessState } = require("../../../lib/build/processState"); -const STExpress = require("../../.."); -const ThirdPartyEmailPasswordRecipe = require("../../../lib/build/recipe/thirdpartyemailpassword").default; -const SessionRecipe = require("../../../lib/build/recipe/session").default; -const { Querier } = require("../../../lib/build/querier"); -const { maxVersion } = require("../../../lib/build/utils"); - -describe(`userIdMapping with ThirdPartyEmailPassword: ${printPath( - "[test/useridmapping/recipeTests/thirdpartyemailpassword.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getUserById", () => { - it("create an emailPassword a thirdParty user and map their userIds, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyEmailPasswordRecipe.init({ - providers: [ - ThirdPartyEmailPasswordRecipe.Google({ - clientId: "google", - clientSecret: "test", - }), - ], - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - { - // create a new EmailPassword User - const email = "test@example.com"; - const password = "testPass123"; - - let signUpResponse = await ThirdPartyEmailPasswordRecipe.emailPasswordSignUp(email, password); - assert.strictEqual(signUpResponse.status, "OK"); - let user = signUpResponse.user; - let superTokensUserId = user.id; - let externalId = "epExternalId"; - - // map the users id - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the users info using the externalId, the id in the response should be the externalId - { - let response = await ThirdPartyEmailPasswordRecipe.getUserById(superTokensUserId); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); - } - } - - { - // create a new ThirdParty user - const email = "test2@example.com"; - - let signUpResponse = await ThirdPartyEmailPasswordRecipe.thirdPartySignInUp("google", "tpId", email); - - // map the users id - let user = signUpResponse.user; - let superTokensUserId = user.id; - let externalId = "tpExternalId"; - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the users info using the externalId, the id in the response should be the externalId - { - let response = await ThirdPartyEmailPasswordRecipe.getUserById(superTokensUserId); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); - } - } - }); - }); -}); diff --git a/test/useridmapping/recipeTests/thirdpartyemailpassword.test.ts b/test/useridmapping/recipeTests/thirdpartyemailpassword.test.ts new file mode 100644 index 000000000..eee0744e3 --- /dev/null +++ b/test/useridmapping/recipeTests/thirdpartyemailpassword.test.ts @@ -0,0 +1,107 @@ +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import ThirdPartyEmailPasswordRecipe from 'supertokens-node/recipe/thirdpartyemailpassword' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +describe(`userIdMapping with ThirdPartyEmailPassword: ${printPath( + '[test/useridmapping/recipeTests/thirdpartyemailpassword.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getUserById', () => { + it('create an emailPassword a thirdParty user and map their userIds, retrieve the user info using getUserById and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyEmailPasswordRecipe.init({ + providers: [ + ThirdPartyEmailPasswordRecipe.Google({ + clientId: 'google', + clientSecret: 'test', + }), + ], + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + { + // create a new EmailPassword User + const email = 'test@example.com' + const password = 'testPass123' + + const signUpResponse = await ThirdPartyEmailPasswordRecipe.emailPasswordSignUp(email, password) + assert.strictEqual(signUpResponse.status, 'OK') + const user = signUpResponse.user + const superTokensUserId = user.id + const externalId = 'epExternalId' + + // map the users id + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the users info using the externalId, the id in the response should be the externalId + { + const response = await ThirdPartyEmailPasswordRecipe.getUserById(superTokensUserId) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.email, email) + } + } + + { + // create a new ThirdParty user + const email = 'test2@example.com' + + const signUpResponse = await ThirdPartyEmailPasswordRecipe.thirdPartySignInUp('google', 'tpId', email) + + // map the users id + const user = signUpResponse.user + const superTokensUserId = user.id + const externalId = 'tpExternalId' + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the users info using the externalId, the id in the response should be the externalId + { + const response = await ThirdPartyEmailPasswordRecipe.getUserById(superTokensUserId) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.email, email) + } + } + }) + }) +}) diff --git a/test/useridmapping/recipeTests/thirdpartypasswordless.test.js b/test/useridmapping/recipeTests/thirdpartypasswordless.test.js deleted file mode 100644 index 7d325d0be..000000000 --- a/test/useridmapping/recipeTests/thirdpartypasswordless.test.js +++ /dev/null @@ -1,120 +0,0 @@ -const assert = require("assert"); -const { printPath, setupST, startST, killAllST, cleanST } = require("../../utils"); -const { ProcessState } = require("../../../lib/build/processState"); -const STExpress = require("../../.."); -const ThirdPartyPasswordlessRecipe = require("../../../lib/build/recipe/thirdpartypasswordless").default; -const SessionRecipe = require("../../../lib/build/recipe/session").default; -const { Querier } = require("../../../lib/build/querier"); -const { maxVersion } = require("../../../lib/build/utils"); - -describe(`userIdMapping with thirdPartyPasswordless: ${printPath( - "[test/useridmapping/recipeTests/thirdpartypasswordless.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getUserById", () => { - it("create a thirdParty and passwordless user and map their userIds, retrieve the user info using getUserById and check that the externalId is returned", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - ThirdPartyPasswordlessRecipe.init({ - contactMethod: "EMAIL_OR_PHONE", - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - createAndSendCustomEmail: (input) => { - return; - }, - createAndSendCustomTextMessage: (input) => { - return; - }, - providers: [ - ThirdPartyPasswordlessRecipe.Google({ - clientId: "google", - clientSecret: "test", - }), - ], - }), - SessionRecipe.init(), - ], - }); - - // Only run for version >= 2.15 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - { - const email = "test2@example.com"; - // create a new ThirdParty user - let signUpResponse = await ThirdPartyPasswordlessRecipe.thirdPartySignInUp("google", "tpId", email); - - // map the users id - let user = signUpResponse.user; - let superTokensUserId = user.id; - let externalId = "tpExternalId"; - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the user info using the externalId, the id in the response should be the externalId - { - let response = await ThirdPartyPasswordlessRecipe.getUserById(superTokensUserId); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - assert.strictEqual(response.email, email); - } - } - - { - // create a Passwordless user - const phoneNumber = "+911234566789"; - const codeInfo = await ThirdPartyPasswordlessRecipe.createCode({ - phoneNumber, - }); - - assert.strictEqual(codeInfo.status, "OK"); - - const consumeCodeResponse = await ThirdPartyPasswordlessRecipe.consumeCode({ - preAuthSessionId: codeInfo.preAuthSessionId, - userInputCode: codeInfo.userInputCode, - deviceId: codeInfo.deviceId, - }); - - assert.strictEqual(consumeCodeResponse.status, "OK"); - - const superTokensUserId = consumeCodeResponse.user.id; - const externalId = "psExternalId"; - - // create the userIdMapping - await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - }); - - // retrieve the user info using the externalId, the id in the response should be the externalId - let response = await ThirdPartyPasswordlessRecipe.getUserById(externalId); - assert.ok(response !== undefined); - assert.strictEqual(response.id, externalId); - } - }); - }); -}); diff --git a/test/useridmapping/recipeTests/thirdpartypasswordless.test.ts b/test/useridmapping/recipeTests/thirdpartypasswordless.test.ts new file mode 100644 index 000000000..8e11b69d5 --- /dev/null +++ b/test/useridmapping/recipeTests/thirdpartypasswordless.test.ts @@ -0,0 +1,120 @@ +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import ThirdPartyPasswordlessRecipe from 'supertokens-node/recipe/thirdpartypasswordless' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../../utils' + +describe(`userIdMapping with thirdPartyPasswordless: ${printPath( + '[test/useridmapping/recipeTests/thirdpartypasswordless.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getUserById', () => { + it('create a thirdParty and passwordless user and map their userIds, retrieve the user info using getUserById and check that the externalId is returned', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + ThirdPartyPasswordlessRecipe.init({ + contactMethod: 'EMAIL_OR_PHONE', + flowType: 'USER_INPUT_CODE_AND_MAGIC_LINK', + createAndSendCustomEmail: (input) => { + + }, + createAndSendCustomTextMessage: (input) => { + + }, + providers: [ + ThirdPartyPasswordlessRecipe.Google({ + clientId: 'google', + clientSecret: 'test', + }), + ], + }), + SessionRecipe.init(), + ], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + { + const email = 'test2@example.com' + // create a new ThirdParty user + const signUpResponse = await ThirdPartyPasswordlessRecipe.thirdPartySignInUp('google', 'tpId', email) + + // map the users id + const user = signUpResponse.user + const superTokensUserId = user.id + const externalId = 'tpExternalId' + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the user info using the externalId, the id in the response should be the externalId + { + const response = await ThirdPartyPasswordlessRecipe.getUserById(superTokensUserId) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + assert.strictEqual(response.email, email) + } + } + + { + // create a Passwordless user + const phoneNumber = '+911234566789' + const codeInfo = await ThirdPartyPasswordlessRecipe.createCode({ + phoneNumber, + }) + + assert.strictEqual(codeInfo.status, 'OK') + + const consumeCodeResponse = await ThirdPartyPasswordlessRecipe.consumeCode({ + preAuthSessionId: codeInfo.preAuthSessionId, + userInputCode: codeInfo.userInputCode, + deviceId: codeInfo.deviceId, + }) + + assert.strictEqual(consumeCodeResponse.status, 'OK') + + const superTokensUserId = consumeCodeResponse.user.id + const externalId = 'psExternalId' + + // create the userIdMapping + await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + }) + + // retrieve the user info using the externalId, the id in the response should be the externalId + const response = await ThirdPartyPasswordlessRecipe.getUserById(externalId) + assert.ok(response !== undefined) + assert.strictEqual(response.id, externalId) + } + }) + }) +}) diff --git a/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js b/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js deleted file mode 100644 index aaa1ab0d8..000000000 --- a/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js +++ /dev/null @@ -1,295 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const EmailPasswordRecipe = require("../../lib/build/recipe/emailpassword").default; -const SessionRecipe = require("../../lib/build/recipe/session").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`updateOrDeleteUserIdMappingInfoTest: ${printPath( - "[test/useridmapping/updateOrDeleteUserIdMappingInfo.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("updateOrDeleteUserIdMappingInfoTest", () => { - it("update externalUserId mapping info with unknown userId", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - { - const response = await STExpress.updateOrDeleteUserIdMappingInfo({ - userId: "unknown", - userIdType: "SUPERTOKENS", - externalUserIdInfo: "someInfo", - }); - assert.strictEqual(Object.keys(response).length, 1); - assert.strictEqual(response.status, "UNKNOWN_MAPPING_ERROR"); - } - - { - const response = await STExpress.updateOrDeleteUserIdMappingInfo({ - userId: "unknown", - userIdType: "EXTERNAL", - externalUserIdInfo: "someInfo", - }); - - assert.strictEqual(Object.keys(response).length, 1); - assert.strictEqual(response.status, "UNKNOWN_MAPPING_ERROR"); - } - - { - const response = await STExpress.updateOrDeleteUserIdMappingInfo({ - userId: "unknown", - userIdType: "ANY", - externalUserIdInfo: "someInfo", - }); - - assert.strictEqual(Object.keys(response).length, 1); - assert.strictEqual(response.status, "UNKNOWN_MAPPING_ERROR"); - } - }); - - it("update externalUserId mapping info with userIdType as SUPERTOKENS and EXTERNAL", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - let externalIdInfo = "externalIdInfo"; - - // create the userId mapping - let createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - externalUserIdInfo: externalIdInfo, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1); - assert.strictEqual(createUserIdMappingResponse.status, "OK"); - - { - // check that the userId mapping exists - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - isValidUserIdMappingResponse(getUserIdMappingResponse, superTokensUserId, externalId, externalIdInfo); - } - - // update the mapping with superTokensUserId and type SUPERTOKENS - { - const newExternalIdInfo = "newExternalIdInfo_1"; - const response = await STExpress.updateOrDeleteUserIdMappingInfo({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - externalUserIdInfo: newExternalIdInfo, - }); - assert.strictEqual(Object.keys(response).length, 1); - assert.strictEqual(response.status, "OK"); - - // retrieve mapping and check that externalUserIdInfo has been updated - { - // check that the userId mapping exists - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - isValidUserIdMappingResponse( - getUserIdMappingResponse, - superTokensUserId, - externalId, - newExternalIdInfo - ); - } - } - - // update the mapping with externalId and type EXTERNAL - { - const newExternalIdInfo = "newExternalIdInfo_2"; - const response = await STExpress.updateOrDeleteUserIdMappingInfo({ - userId: externalId, - userIdType: "EXTERNAL", - externalUserIdInfo: newExternalIdInfo, - }); - assert.strictEqual(Object.keys(response).length, 1); - assert.strictEqual(response.status, "OK"); - - // retrieve mapping and check that externalUserIdInfo has been updated - { - // check that the userId mapping exists - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - }); - isValidUserIdMappingResponse( - getUserIdMappingResponse, - superTokensUserId, - externalId, - newExternalIdInfo - ); - } - } - }); - - it("update externalUserId mapping info with userIdType as ANY", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.15 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.14") === "2.14") { - return this.skip(); - } - - // create a user - let signUpResponse = await EmailPasswordRecipe.signUp("test@example.com", "testPass123"); - assert.strictEqual(signUpResponse.status, "OK"); - - let superTokensUserId = signUpResponse.user.id; - let externalId = "externalId"; - let externalIdInfo = "externalIdInfo"; - - // create the userId mapping - let createUserIdMappingResponse = await STExpress.createUserIdMapping({ - superTokensUserId, - externalUserId: externalId, - externalUserIdInfo: externalIdInfo, - }); - assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1); - assert.strictEqual(createUserIdMappingResponse.status, "OK"); - - { - // check that the userId mapping exists - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - }); - isValidUserIdMappingResponse(getUserIdMappingResponse, superTokensUserId, externalId, externalIdInfo); - } - - // update the mapping with superTokensUserId and type ANY - { - const newExternalIdInfo = "newExternalIdInfo_1"; - const response = await STExpress.updateOrDeleteUserIdMappingInfo({ - userId: superTokensUserId, - userIdType: "SUPERTOKENS", - externalUserIdInfo: newExternalIdInfo, - }); - assert.strictEqual(Object.keys(response).length, 1); - assert.strictEqual(response.status, "OK"); - - // retrieve mapping and check that externalUserIdInfo has been updated - { - // check that the userId mapping exists - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: superTokensUserId, - userIdType: "ANY", - }); - isValidUserIdMappingResponse( - getUserIdMappingResponse, - superTokensUserId, - externalId, - newExternalIdInfo - ); - } - } - - // update the mapping with externalId and type ANY - { - const newExternalIdInfo = "newExternalIdInfo_2"; - const response = await STExpress.updateOrDeleteUserIdMappingInfo({ - userId: externalId, - userIdType: "ANY", - externalUserIdInfo: newExternalIdInfo, - }); - assert.strictEqual(Object.keys(response).length, 1); - assert.strictEqual(response.status, "OK"); - - // retrieve mapping and check that externalUserIdInfo has been updated - { - // check that the userId mapping exists - let getUserIdMappingResponse = await STExpress.getUserIdMapping({ - userId: externalId, - userIdType: "EXTERNAL", - }); - isValidUserIdMappingResponse( - getUserIdMappingResponse, - superTokensUserId, - externalId, - newExternalIdInfo - ); - } - } - }); - }); - - function isValidUserIdMappingResponse(userIdMapping, superTokensUserId, externalId, externalIdInfo) { - assert.strictEqual(Object.keys(userIdMapping).length, 4); - assert.strictEqual(userIdMapping.status, "OK"); - assert.strictEqual(userIdMapping.superTokensUserId, superTokensUserId); - assert.strictEqual(userIdMapping.externalUserId, externalId); - assert.strictEqual(userIdMapping.externalUserIdInfo, externalIdInfo); - } -}); diff --git a/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.ts b/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.ts new file mode 100644 index 000000000..ce2aca90c --- /dev/null +++ b/test/useridmapping/updateOrDeleteUserIdMappingInfo.test.ts @@ -0,0 +1,292 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword' +import SessionRecipe from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`updateOrDeleteUserIdMappingInfoTest: ${printPath( + '[test/useridmapping/updateOrDeleteUserIdMappingInfo.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('updateOrDeleteUserIdMappingInfoTest', () => { + it('update externalUserId mapping info with unknown userId', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + { + const response = await STExpress.updateOrDeleteUserIdMappingInfo({ + userId: 'unknown', + userIdType: 'SUPERTOKENS', + externalUserIdInfo: 'someInfo', + }) + assert.strictEqual(Object.keys(response).length, 1) + assert.strictEqual(response.status, 'UNKNOWN_MAPPING_ERROR') + } + + { + const response = await STExpress.updateOrDeleteUserIdMappingInfo({ + userId: 'unknown', + userIdType: 'EXTERNAL', + externalUserIdInfo: 'someInfo', + }) + + assert.strictEqual(Object.keys(response).length, 1) + assert.strictEqual(response.status, 'UNKNOWN_MAPPING_ERROR') + } + + { + const response = await STExpress.updateOrDeleteUserIdMappingInfo({ + userId: 'unknown', + userIdType: 'ANY', + externalUserIdInfo: 'someInfo', + }) + + assert.strictEqual(Object.keys(response).length, 1) + assert.strictEqual(response.status, 'UNKNOWN_MAPPING_ERROR') + } + }) + + it('update externalUserId mapping info with userIdType as SUPERTOKENS and EXTERNAL', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + const externalIdInfo = 'externalIdInfo' + + // create the userId mapping + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + externalUserIdInfo: externalIdInfo, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1) + assert.strictEqual(createUserIdMappingResponse.status, 'OK') + + { + // check that the userId mapping exists + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + isValidUserIdMappingResponse(getUserIdMappingResponse, superTokensUserId, externalId, externalIdInfo) + } + + // update the mapping with superTokensUserId and type SUPERTOKENS + { + const newExternalIdInfo = 'newExternalIdInfo_1' + const response = await STExpress.updateOrDeleteUserIdMappingInfo({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + externalUserIdInfo: newExternalIdInfo, + }) + assert.strictEqual(Object.keys(response).length, 1) + assert.strictEqual(response.status, 'OK') + + // retrieve mapping and check that externalUserIdInfo has been updated + { + // check that the userId mapping exists + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + isValidUserIdMappingResponse( + getUserIdMappingResponse, + superTokensUserId, + externalId, + newExternalIdInfo, + ) + } + } + + // update the mapping with externalId and type EXTERNAL + { + const newExternalIdInfo = 'newExternalIdInfo_2' + const response = await STExpress.updateOrDeleteUserIdMappingInfo({ + userId: externalId, + userIdType: 'EXTERNAL', + externalUserIdInfo: newExternalIdInfo, + }) + assert.strictEqual(Object.keys(response).length, 1) + assert.strictEqual(response.status, 'OK') + + // retrieve mapping and check that externalUserIdInfo has been updated + { + // check that the userId mapping exists + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + }) + isValidUserIdMappingResponse( + getUserIdMappingResponse, + superTokensUserId, + externalId, + newExternalIdInfo, + ) + } + } + }) + + it('update externalUserId mapping info with userIdType as ANY', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [EmailPasswordRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.15 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.14') === '2.14') + return this.skip() + + // create a user + const signUpResponse = await EmailPasswordRecipe.signUp('test@example.com', 'testPass123') + assert.strictEqual(signUpResponse.status, 'OK') + + const superTokensUserId = signUpResponse.user.id + const externalId = 'externalId' + const externalIdInfo = 'externalIdInfo' + + // create the userId mapping + const createUserIdMappingResponse = await STExpress.createUserIdMapping({ + superTokensUserId, + externalUserId: externalId, + externalUserIdInfo: externalIdInfo, + }) + assert.strictEqual(Object.keys(createUserIdMappingResponse).length, 1) + assert.strictEqual(createUserIdMappingResponse.status, 'OK') + + { + // check that the userId mapping exists + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + }) + isValidUserIdMappingResponse(getUserIdMappingResponse, superTokensUserId, externalId, externalIdInfo) + } + + // update the mapping with superTokensUserId and type ANY + { + const newExternalIdInfo = 'newExternalIdInfo_1' + const response = await STExpress.updateOrDeleteUserIdMappingInfo({ + userId: superTokensUserId, + userIdType: 'SUPERTOKENS', + externalUserIdInfo: newExternalIdInfo, + }) + assert.strictEqual(Object.keys(response).length, 1) + assert.strictEqual(response.status, 'OK') + + // retrieve mapping and check that externalUserIdInfo has been updated + { + // check that the userId mapping exists + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: superTokensUserId, + userIdType: 'ANY', + }) + isValidUserIdMappingResponse( + getUserIdMappingResponse, + superTokensUserId, + externalId, + newExternalIdInfo, + ) + } + } + + // update the mapping with externalId and type ANY + { + const newExternalIdInfo = 'newExternalIdInfo_2' + const response = await STExpress.updateOrDeleteUserIdMappingInfo({ + userId: externalId, + userIdType: 'ANY', + externalUserIdInfo: newExternalIdInfo, + }) + assert.strictEqual(Object.keys(response).length, 1) + assert.strictEqual(response.status, 'OK') + + // retrieve mapping and check that externalUserIdInfo has been updated + { + // check that the userId mapping exists + const getUserIdMappingResponse = await STExpress.getUserIdMapping({ + userId: externalId, + userIdType: 'EXTERNAL', + }) + isValidUserIdMappingResponse( + getUserIdMappingResponse, + superTokensUserId, + externalId, + newExternalIdInfo, + ) + } + } + }) + }) + + function isValidUserIdMappingResponse(userIdMapping, superTokensUserId, externalId, externalIdInfo) { + assert.strictEqual(Object.keys(userIdMapping).length, 4) + assert.strictEqual(userIdMapping.status, 'OK') + assert.strictEqual(userIdMapping.superTokensUserId, superTokensUserId) + assert.strictEqual(userIdMapping.externalUserId, externalId) + assert.strictEqual(userIdMapping.externalUserIdInfo, externalIdInfo) + } +}) diff --git a/test/usermetadata/clearUserMetadata.test.js b/test/usermetadata/clearUserMetadata.test.js deleted file mode 100644 index f15f4e922..000000000 --- a/test/usermetadata/clearUserMetadata.test.js +++ /dev/null @@ -1,90 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserMetadataRecipe = require("../../lib/build/recipe/usermetadata").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`clearUserMetadataTest: ${printPath("[test/usermetadata/clearUserMetadata.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("clearUserMetadata", () => { - it("should return OK for unknown user id", async function () { - await startST(); - - const testUserId = "userId"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - const result = await UserMetadataRecipe.clearUserMetadata(testUserId); - - assert.strictEqual(result.status, "OK"); - }); - - it("should clear stored userId", async function () { - await startST(); - - const testUserId = "userId"; - const testMetadata = { - role: "admin", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata); - - const result = await UserMetadataRecipe.clearUserMetadata(testUserId); - - assert.strictEqual(result.status, "OK"); - - const getResult = await UserMetadataRecipe.getUserMetadata(testUserId); - - assert.strictEqual(getResult.status, "OK"); - assert.deepStrictEqual(getResult.metadata, {}); - }); - }); -}); diff --git a/test/usermetadata/clearUserMetadata.test.ts b/test/usermetadata/clearUserMetadata.test.ts new file mode 100644 index 000000000..c4ccfcb4c --- /dev/null +++ b/test/usermetadata/clearUserMetadata.test.ts @@ -0,0 +1,89 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`clearUserMetadataTest: ${printPath('[test/usermetadata/clearUserMetadata.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('clearUserMetadata', () => { + it('should return OK for unknown user id', async () => { + await startST() + + const testUserId = 'userId' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return it.skip() + + const result = await UserMetadataRecipe.clearUserMetadata(testUserId) + + assert.strictEqual(result.status, 'OK') + }) + + it('should clear stored userId', async function () { + await startST() + + const testUserId = 'userId' + const testMetadata = { + role: 'admin', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata) + + const result = await UserMetadataRecipe.clearUserMetadata(testUserId) + + assert.strictEqual(result.status, 'OK') + + const getResult = await UserMetadataRecipe.getUserMetadata(testUserId) + + assert.strictEqual(getResult.status, 'OK') + assert.deepStrictEqual(getResult.metadata, {}) + }) + }) +}) diff --git a/test/usermetadata/config.test.js b/test/usermetadata/config.test.js deleted file mode 100644 index 5c20e7eeb..000000000 --- a/test/usermetadata/config.test.js +++ /dev/null @@ -1,45 +0,0 @@ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -const { ProcessState } = require("../../lib/build/processState"); -const STExpress = require("../.."); -const UserMetadataRecipe = require("../../lib/build/recipe/usermetadata/recipe").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`configTest: ${printPath("[test/usermetadata/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("recipe init", () => { - it("should work fine without config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - await UserMetadataRecipe.getInstanceOrThrowError(); - }); - }); -}); diff --git a/test/usermetadata/config.test.ts b/test/usermetadata/config.test.ts new file mode 100644 index 000000000..999b50bbb --- /dev/null +++ b/test/usermetadata/config.test.ts @@ -0,0 +1,45 @@ +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata/recipe' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`configTest: ${printPath('[test/usermetadata/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('recipe init', () => { + it('should work fine without config', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + await UserMetadataRecipe.getInstanceOrThrowError() + }) + }) +}) diff --git a/test/usermetadata/getUserMetadata.test.js b/test/usermetadata/getUserMetadata.test.js deleted file mode 100644 index 9c116dec9..000000000 --- a/test/usermetadata/getUserMetadata.test.js +++ /dev/null @@ -1,87 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -const STExpress = require("../../"); -const { ProcessState } = require("../../lib/build/processState"); -const UserMetadataRecipe = require("../../lib/build/recipe/usermetadata").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`getUserMetadataTest: ${printPath("[test/usermetadata/getUserMetadata.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getUserMetadata", () => { - it("should return an empty object for unknown userIds", async function () { - await startST(); - - const testUserId = "userId"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - const result = await UserMetadataRecipe.getUserMetadata(testUserId); - - assert.strictEqual(result.status, "OK"); - assert.deepStrictEqual(result.metadata, {}); - }); - - it("should return an object if it's created.", async function () { - await startST(); - - const testUserId = "userId"; - const testMetadata = { - role: "admin", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata); - - const result = await UserMetadataRecipe.getUserMetadata(testUserId); - - assert.strictEqual(result.status, "OK"); - assert.deepStrictEqual(result.metadata, testMetadata); - }); - }); -}); diff --git a/test/usermetadata/getUserMetadata.test.ts b/test/usermetadata/getUserMetadata.test.ts new file mode 100644 index 000000000..8b5380167 --- /dev/null +++ b/test/usermetadata/getUserMetadata.test.ts @@ -0,0 +1,86 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getUserMetadataTest: ${printPath('[test/usermetadata/getUserMetadata.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getUserMetadata', () => { + it('should return an empty object for unknown userIds', async function () { + await startST() + + const testUserId = 'userId' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + const result = await UserMetadataRecipe.getUserMetadata(testUserId) + + assert.strictEqual(result.status, 'OK') + assert.deepStrictEqual(result.metadata, {}) + }) + + it('should return an object if it\'s created.', async function () { + await startST() + + const testUserId = 'userId' + const testMetadata = { + role: 'admin', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata) + + const result = await UserMetadataRecipe.getUserMetadata(testUserId) + + assert.strictEqual(result.status, 'OK') + assert.deepStrictEqual(result.metadata, testMetadata) + }) + }) +}) diff --git a/test/usermetadata/override.test.js b/test/usermetadata/override.test.js deleted file mode 100644 index d03e53f0d..000000000 --- a/test/usermetadata/override.test.js +++ /dev/null @@ -1,144 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -const STExpress = require("../../"); -const { ProcessState } = require("../../lib/build/processState"); -const UserMetadataRecipe = require("../../lib/build/recipe/usermetadata").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`overrideTest: ${printPath("[test/usermetadata/override.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("recipe functions", () => { - it("should work without an override config", async function () { - await startST(); - - const testUserId = "userId"; - const testUserContext = { hello: ":)" }; - const testMetadata = { - role: "admin", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata, testUserContext); - const getResult = await UserMetadataRecipe.getUserMetadata(testUserId, testUserContext); - const clearResult = await UserMetadataRecipe.clearUserMetadata(testUserId, testUserContext); - - assert.deepStrictEqual(updateResult.metadata, testMetadata); - assert.deepStrictEqual(getResult.metadata, testMetadata); - assert.deepStrictEqual(clearResult.status, "OK"); - }); - - it("should call user provided overrides", async function () { - await startST(); - - const testUserId = "userId"; - const testUserContext = { hello: ":)" }; - const testMetadata = { - role: "admin", - }; - - let getUserMetadataResp = undefined; - let updateUserMetadataResp = undefined; - let clearUserMetadataResp = undefined; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - UserMetadataRecipe.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getUserMetadata: async (input) => { - assert.strictEqual(input.userId, testUserId); - // This is intentionally strictEqual, we expect them to be the same object not a clone. - assert.strictEqual(input.userContext, testUserContext); - getUserMetadataResp = await originalImplementation.getUserMetadata(input); - - return getUserMetadataResp; - }, - updateUserMetadata: async (input) => { - assert.strictEqual(input.userId, testUserId); - // These are intentionally strictEquals, we expect them to be the same object not a clone. - assert.strictEqual(input.metadataUpdate, testMetadata); - assert.strictEqual(input.userContext, testUserContext); - updateUserMetadataResp = await originalImplementation.updateUserMetadata(input); - - return updateUserMetadataResp; - }, - clearUserMetadata: async (input) => { - assert.strictEqual(input.userId, testUserId); - // This is intentionally strictEqual, we expect them to be the same object not a clone. - assert.strictEqual(input.userContext, testUserContext); - clearUserMetadataResp = await originalImplementation.clearUserMetadata(input); - - return clearUserMetadataResp; - }, - }; - }, - }, - }), - ], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata, testUserContext); - const getResult = await UserMetadataRecipe.getUserMetadata(testUserId, testUserContext); - const clearResult = await UserMetadataRecipe.clearUserMetadata(testUserId, testUserContext); - - assert.deepStrictEqual(updateResult.metadata, testMetadata); - // This is intentionally strictEqual, we expect them to be the same object not a clone. - assert.strictEqual(updateUserMetadataResp, updateResult); - - assert.deepStrictEqual(getResult.metadata, testMetadata); - // This is intentionally strictEqual, we expect them to be the same object not a clone. - assert.strictEqual(getUserMetadataResp, getResult); - - assert.deepStrictEqual(clearResult.status, "OK"); - // This is intentionally strictEqual, we expect them to be the same object not a clone. - assert.strictEqual(clearUserMetadataResp, clearResult); - }); - }); -}); diff --git a/test/usermetadata/override.test.ts b/test/usermetadata/override.test.ts new file mode 100644 index 000000000..6b24854bb --- /dev/null +++ b/test/usermetadata/override.test.ts @@ -0,0 +1,142 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`overrideTest: ${printPath('[test/usermetadata/override.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('recipe functions', () => { + it('should work without an override config', async function () { + await startST() + + const testUserId = 'userId' + const testUserContext = { hello: ':)' } + const testMetadata = { + role: 'admin', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata, testUserContext) + const getResult = await UserMetadataRecipe.getUserMetadata(testUserId, testUserContext) + const clearResult = await UserMetadataRecipe.clearUserMetadata(testUserId, testUserContext) + + assert.deepStrictEqual(updateResult.metadata, testMetadata) + assert.deepStrictEqual(getResult.metadata, testMetadata) + assert.deepStrictEqual(clearResult.status, 'OK') + }) + + it('should call user provided overrides', async function () { + await startST() + + const testUserId = 'userId' + const testUserContext = { hello: ':)' } + const testMetadata = { + role: 'admin', + } + + let getUserMetadataResp + let updateUserMetadataResp + let clearUserMetadataResp + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + UserMetadataRecipe.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getUserMetadata: async (input) => { + assert.strictEqual(input.userId, testUserId) + // This is intentionally strictEqual, we expect them to be the same object not a clone. + assert.strictEqual(input.userContext, testUserContext) + getUserMetadataResp = await originalImplementation.getUserMetadata(input) + + return getUserMetadataResp + }, + updateUserMetadata: async (input) => { + assert.strictEqual(input.userId, testUserId) + // These are intentionally strictEquals, we expect them to be the same object not a clone. + assert.strictEqual(input.metadataUpdate, testMetadata) + assert.strictEqual(input.userContext, testUserContext) + updateUserMetadataResp = await originalImplementation.updateUserMetadata(input) + + return updateUserMetadataResp + }, + clearUserMetadata: async (input) => { + assert.strictEqual(input.userId, testUserId) + // This is intentionally strictEqual, we expect them to be the same object not a clone. + assert.strictEqual(input.userContext, testUserContext) + clearUserMetadataResp = await originalImplementation.clearUserMetadata(input) + + return clearUserMetadataResp + }, + } + }, + }, + }), + ], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata, testUserContext) + const getResult = await UserMetadataRecipe.getUserMetadata(testUserId, testUserContext) + const clearResult = await UserMetadataRecipe.clearUserMetadata(testUserId, testUserContext) + + assert.deepStrictEqual(updateResult.metadata, testMetadata) + // This is intentionally strictEqual, we expect them to be the same object not a clone. + assert.strictEqual(updateUserMetadataResp, updateResult) + + assert.deepStrictEqual(getResult.metadata, testMetadata) + // This is intentionally strictEqual, we expect them to be the same object not a clone. + assert.strictEqual(getUserMetadataResp, getResult) + + assert.deepStrictEqual(clearResult.status, 'OK') + // This is intentionally strictEqual, we expect them to be the same object not a clone. + assert.strictEqual(clearUserMetadataResp, clearResult) + }) + }) +}) diff --git a/test/usermetadata/updateUserMetadata.test.js b/test/usermetadata/updateUserMetadata.test.js deleted file mode 100644 index a67fabf6f..000000000 --- a/test/usermetadata/updateUserMetadata.test.js +++ /dev/null @@ -1,198 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserMetadataRecipe = require("../../lib/build/recipe/usermetadata").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`updateUserMetadataTest: ${printPath("[test/usermetadata/updateUserMetadata.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("updateUserMetadata", () => { - it("should create metadata for unknown user id", async function () { - await startST(); - - const testUserId = "userId"; - const testMetadata = { - role: "admin", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata); - - const getResult = await UserMetadataRecipe.getUserMetadata(testUserId); - - assert.strictEqual(updateResult.status, "OK"); - assert.deepStrictEqual(updateResult.metadata, testMetadata); - assert.strictEqual(getResult.status, "OK"); - assert.deepStrictEqual(getResult.metadata, testMetadata); - }); - - it("should create metadata with utf8 encoding", async function () { - await startST(); - - const testUserId = "userId"; - const testMetadata = { - role: "\uFDFD Æää", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata); - - const getResult = await UserMetadataRecipe.getUserMetadata(testUserId); - - assert.strictEqual(updateResult.status, "OK"); - assert.deepStrictEqual(updateResult.metadata, testMetadata); - assert.strictEqual(getResult.status, "OK"); - assert.deepStrictEqual(getResult.metadata, testMetadata); - }); - - it("should create metadata for cleared user id", async function () { - await startST(); - - const testUserId = "userId"; - const testMetadata = { - role: "admin", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - await UserMetadataRecipe.updateUserMetadata(testUserId, { test: "asdf" }); - await UserMetadataRecipe.clearUserMetadata(testUserId); - const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata); - - const getResult = await UserMetadataRecipe.getUserMetadata(testUserId); - - assert.strictEqual(updateResult.status, "OK"); - assert.deepStrictEqual(updateResult.metadata, testMetadata); - - assert.strictEqual(getResult.status, "OK"); - assert.deepStrictEqual(getResult.metadata, testMetadata); - }); - - it("should update metadata by shallow merge", async function () { - await startST(); - - const testUserId = "userId"; - const testMetadata = { - updated: { - subObjectNull: "this will become null", - subObjectCleared: "this will be removed", - subObjectUpdate: "this will become a number", - }, - cleared: "this should not be on the end result", - }; - const testMetadataUpdate = { - updated: { - subObjectNull: null, - subObjectUpdate: 123, - subObjectNewProp: "this will appear", - }, - cleared: null, - newRootProp: "this should appear on the end result", - }; - const expectedResult = { - updated: { - subObjectNull: null, - subObjectUpdate: 123, - subObjectNewProp: "this will appear", - }, - newRootProp: "this should appear on the end result", - }; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserMetadataRecipe.init()], - }); - - // Only run for version >= 2.13 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.12") === "2.12") { - return this.skip(); - } - - await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata); - const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadataUpdate); - - const getResult = await UserMetadataRecipe.getUserMetadata(testUserId); - - assert.strictEqual(updateResult.status, "OK"); - assert.deepStrictEqual(updateResult.metadata, expectedResult); - - assert.strictEqual(getResult.status, "OK"); - assert.deepStrictEqual(getResult.metadata, expectedResult); - }); - }); -}); diff --git a/test/usermetadata/updateUserMetadata.test.ts b/test/usermetadata/updateUserMetadata.test.ts new file mode 100644 index 000000000..602c9c08a --- /dev/null +++ b/test/usermetadata/updateUserMetadata.test.ts @@ -0,0 +1,194 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`updateUserMetadataTest: ${printPath('[test/usermetadata/updateUserMetadata.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('updateUserMetadata', () => { + it('should create metadata for unknown user id', async function () { + await startST() + + const testUserId = 'userId' + const testMetadata = { + role: 'admin', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata) + + const getResult = await UserMetadataRecipe.getUserMetadata(testUserId) + + assert.strictEqual(updateResult.status, 'OK') + assert.deepStrictEqual(updateResult.metadata, testMetadata) + assert.strictEqual(getResult.status, 'OK') + assert.deepStrictEqual(getResult.metadata, testMetadata) + }) + + it('should create metadata with utf8 encoding', async function () { + await startST() + + const testUserId = 'userId' + const testMetadata = { + role: '\uFDFD Æää', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata) + + const getResult = await UserMetadataRecipe.getUserMetadata(testUserId) + + assert.strictEqual(updateResult.status, 'OK') + assert.deepStrictEqual(updateResult.metadata, testMetadata) + assert.strictEqual(getResult.status, 'OK') + assert.deepStrictEqual(getResult.metadata, testMetadata) + }) + + it('should create metadata for cleared user id', async function () { + await startST() + + const testUserId = 'userId' + const testMetadata = { + role: 'admin', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + await UserMetadataRecipe.updateUserMetadata(testUserId, { test: 'asdf' }) + await UserMetadataRecipe.clearUserMetadata(testUserId) + const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata) + + const getResult = await UserMetadataRecipe.getUserMetadata(testUserId) + + assert.strictEqual(updateResult.status, 'OK') + assert.deepStrictEqual(updateResult.metadata, testMetadata) + + assert.strictEqual(getResult.status, 'OK') + assert.deepStrictEqual(getResult.metadata, testMetadata) + }) + + it('should update metadata by shallow merge', async function () { + await startST() + + const testUserId = 'userId' + const testMetadata = { + updated: { + subObjectNull: 'this will become null', + subObjectCleared: 'this will be removed', + subObjectUpdate: 'this will become a number', + }, + cleared: 'this should not be on the end result', + } + const testMetadataUpdate = { + updated: { + subObjectNull: null, + subObjectUpdate: 123, + subObjectNewProp: 'this will appear', + }, + cleared: null, + newRootProp: 'this should appear on the end result', + } + const expectedResult = { + updated: { + subObjectNull: null, + subObjectUpdate: 123, + subObjectNewProp: 'this will appear', + }, + newRootProp: 'this should appear on the end result', + } + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserMetadataRecipe.init()], + }) + + // Only run for version >= 2.13 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.12') === '2.12') + return this.skip() + + await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadata) + const updateResult = await UserMetadataRecipe.updateUserMetadata(testUserId, testMetadataUpdate) + + const getResult = await UserMetadataRecipe.getUserMetadata(testUserId) + + assert.strictEqual(updateResult.status, 'OK') + assert.deepStrictEqual(updateResult.metadata, expectedResult) + + assert.strictEqual(getResult.status, 'OK') + assert.deepStrictEqual(getResult.metadata, expectedResult) + }) + }) +}) diff --git a/test/userroles/addRoleToUser.test.js b/test/userroles/addRoleToUser.test.js deleted file mode 100644 index 31fefa746..000000000 --- a/test/userroles/addRoleToUser.test.js +++ /dev/null @@ -1,159 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`addRoleToUserTest: ${printPath("[test/userroles/addRoleToUser.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("addRoleToUserTest", () => { - it("add a role to a user", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const userId = "userId"; - const role = "role"; - - // create a new role - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // add the role to a user - { - const result = await UserRolesRecipe.addRoleToUser(userId, role); - assert.strictEqual(result.status, "OK"); - assert(!result.didUserAlreadyHaveRole); - } - - // check that user has role - { - const result = await UserRolesRecipe.getRolesForUser(userId); - assert.strictEqual(result.status, "OK"); - assert.strictEqual(result.roles.length, 1); - assert.strictEqual(result.roles[0], role); - } - }); - - it("add duplicate role to the user", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const userId = "userId"; - const role = "role"; - - // create a new role - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // add the role to a user - { - const result = await UserRolesRecipe.addRoleToUser(userId, role); - assert.strictEqual(result.status, "OK"); - assert(!result.didUserAlreadyHaveRole); - } - - // add the same role to the user - { - const result = await UserRolesRecipe.addRoleToUser(userId, role); - assert.strictEqual(result.status, "OK"); - assert(result.didUserAlreadyHaveRole); - } - - // check that user has role - { - const result = await UserRolesRecipe.getRolesForUser(userId); - assert.strictEqual(result.status, "OK"); - assert.strictEqual(result.roles.length, 1); - assert.strictEqual(result.roles[0], role); - } - }); - - it("add unknown role to the user", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const userId = "userId"; - const role = "unknownRole"; - - // add the unknown role to the user - { - const result = await UserRolesRecipe.addRoleToUser(userId, role); - assert.strictEqual(result.status, "UNKNOWN_ROLE_ERROR"); - } - }); - }); -}); diff --git a/test/userroles/addRoleToUser.test.ts b/test/userroles/addRoleToUser.test.ts new file mode 100644 index 000000000..5c17a1b78 --- /dev/null +++ b/test/userroles/addRoleToUser.test.ts @@ -0,0 +1,156 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`addRoleToUserTest: ${printPath('[test/userroles/addRoleToUser.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('addRoleToUserTest', () => { + it('add a role to a user', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const userId = 'userId' + const role = 'role' + + // create a new role + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // add the role to a user + { + const result = await UserRolesRecipe.addRoleToUser(userId, role) + assert.strictEqual(result.status, 'OK') + assert(!result.didUserAlreadyHaveRole) + } + + // check that user has role + { + const result = await UserRolesRecipe.getRolesForUser(userId) + assert.strictEqual(result.status, 'OK') + assert.strictEqual(result.roles.length, 1) + assert.strictEqual(result.roles[0], role) + } + }) + + it('add duplicate role to the user', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const userId = 'userId' + const role = 'role' + + // create a new role + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // add the role to a user + { + const result = await UserRolesRecipe.addRoleToUser(userId, role) + assert.strictEqual(result.status, 'OK') + assert(!result.didUserAlreadyHaveRole) + } + + // add the same role to the user + { + const result = await UserRolesRecipe.addRoleToUser(userId, role) + assert.strictEqual(result.status, 'OK') + assert(result.didUserAlreadyHaveRole) + } + + // check that user has role + { + const result = await UserRolesRecipe.getRolesForUser(userId) + assert.strictEqual(result.status, 'OK') + assert.strictEqual(result.roles.length, 1) + assert.strictEqual(result.roles[0], role) + } + }) + + it('add unknown role to the user', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const userId = 'userId' + const role = 'unknownRole' + + // add the unknown role to the user + { + const result = await UserRolesRecipe.addRoleToUser(userId, role) + assert.strictEqual(result.status, 'UNKNOWN_ROLE_ERROR') + } + }) + }) +}) diff --git a/test/userroles/claims.test.js b/test/userroles/claims.test.js deleted file mode 100644 index 241e92a31..000000000 --- a/test/userroles/claims.test.js +++ /dev/null @@ -1,264 +0,0 @@ -const assert = require("assert"); -const { printPath, setupST, startST, killAllST, cleanST, mockResponse, mockRequest } = require("../utils"); -const { ProcessState } = require("../../lib/build/processState"); -const STExpress = require("../.."); -const UserRoles = require("../../lib/build/recipe/userroles").default; -const Session = require("../../lib/build/recipe/session").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); - -describe(`claimsTest: ${printPath("[test/userroles/claims.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("recipe init", () => { - it("should add claims to session without config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const session = await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - assert.deepStrictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), []); - assert.deepStrictEqual(await session.getClaimValue(UserRoles.PermissionClaim), []); - }); - - it("should not add claims if disabled in config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - UserRoles.init({ - skipAddingPermissionsToAccessToken: true, - skipAddingRolesToAccessToken: true, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const session = await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - assert.strictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), undefined); - assert.strictEqual(await session.getClaimValue(UserRoles.PermissionClaim), undefined); - }); - - it("should add claims to session with values", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); - await UserRoles.addRoleToUser("userId", "test"); - const session = await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - assert.deepStrictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), ["test"]); - assert.deepStrictEqual(await session.getClaimValue(UserRoles.PermissionClaim), ["a", "b"]); - }); - }); - - describe("validation", () => { - it("should validate roles", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); - await UserRoles.addRoleToUser("userId", "test"); - const session = await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - - await session.assertClaims([UserRoles.UserRoleClaim.validators.includes("test")]); - - let err; - try { - await session.assertClaims([UserRoles.UserRoleClaim.validators.includes("nope")]); - } catch (ex) { - console.log(ex); - err = ex; - } - assert.ok(err); - assert.strictEqual(err.type, "INVALID_CLAIMS"); - assert.strictEqual(err.payload.length, 1); - assert.strictEqual(err.payload[0].id, "st-role"); - assert.deepStrictEqual(err.payload[0].reason, { - message: "wrong value", - expectedToInclude: "nope", - actualValue: ["test"], - }); - }); - it("should validate roles after refetching", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - UserRoles.init({ - skipAddingRolesToAccessToken: true, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const session = await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); - await UserRoles.addRoleToUser("userId", "test"); - - await session.assertClaims([UserRoles.UserRoleClaim.validators.includes("test")]); - }); - it("should validate permissions", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => "cookie" })], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); - await UserRoles.addRoleToUser("userId", "test"); - const session = await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - - await session.assertClaims([UserRoles.PermissionClaim.validators.includes("a")]); - - let err; - try { - await session.assertClaims([UserRoles.PermissionClaim.validators.includes("nope")]); - } catch (ex) { - console.log(ex); - err = ex; - } - assert.ok(err); - assert.strictEqual(err.type, "INVALID_CLAIMS"); - assert.strictEqual(err.payload.length, 1); - assert.strictEqual(err.payload[0].id, "st-perm"); - assert.deepStrictEqual(err.payload[0].reason, { - message: "wrong value", - expectedToInclude: "nope", - actualValue: ["a", "b"], - }); - }); - it("should validate permissions after refetching", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [ - UserRoles.init({ - skipAddingPermissionsToAccessToken: true, - }), - Session.init({ getTokenTransferMethod: () => "cookie" }), - ], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const session = await Session.createNewSession(mockRequest(), mockResponse(), "userId"); - await UserRoles.createNewRoleOrAddPermissions("test", ["a", "b"]); - await UserRoles.addRoleToUser("userId", "test"); - - await session.assertClaims([UserRoles.PermissionClaim.validators.includes("a")]); - }); - }); -}); diff --git a/test/userroles/claims.test.ts b/test/userroles/claims.test.ts new file mode 100644 index 000000000..24b20e257 --- /dev/null +++ b/test/userroles/claims.test.ts @@ -0,0 +1,260 @@ +import assert from 'assert' +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import UserRoles from 'supertokens-node/recipe/userroles' +import Session from 'supertokens-node/recipe/session' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, mockRequest, mockResponse, printPath, setupST, startST } from '../utils' + +describe(`claimsTest: ${printPath('[test/userroles/claims.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('recipe init', () => { + it('should add claims to session without config', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + assert.deepStrictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), []) + assert.deepStrictEqual(await session.getClaimValue(UserRoles.PermissionClaim), []) + }) + + it('should not add claims if disabled in config', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + UserRoles.init({ + skipAddingPermissionsToAccessToken: true, + skipAddingRolesToAccessToken: true, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + assert.strictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), undefined) + assert.strictEqual(await session.getClaimValue(UserRoles.PermissionClaim), undefined) + }) + + it('should add claims to session with values', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + await UserRoles.createNewRoleOrAddPermissions('test', ['a', 'b']) + await UserRoles.addRoleToUser('userId', 'test') + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + assert.deepStrictEqual(await session.getClaimValue(UserRoles.UserRoleClaim), ['test']) + assert.deepStrictEqual(await session.getClaimValue(UserRoles.PermissionClaim), ['a', 'b']) + }) + }) + + describe('validation', () => { + it('should validate roles', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + await UserRoles.createNewRoleOrAddPermissions('test', ['a', 'b']) + await UserRoles.addRoleToUser('userId', 'test') + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + + await session.assertClaims([UserRoles.UserRoleClaim.validators.includes('test')]) + + let err + try { + await session.assertClaims([UserRoles.UserRoleClaim.validators.includes('nope')]) + } + catch (ex) { + console.log(ex) + err = ex + } + assert.ok(err) + assert.strictEqual(err.type, 'INVALID_CLAIMS') + assert.strictEqual(err.payload.length, 1) + assert.strictEqual(err.payload[0].id, 'st-role') + assert.deepStrictEqual(err.payload[0].reason, { + message: 'wrong value', + expectedToInclude: 'nope', + actualValue: ['test'], + }) + }) + it('should validate roles after refetching', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + UserRoles.init({ + skipAddingRolesToAccessToken: true, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + await UserRoles.createNewRoleOrAddPermissions('test', ['a', 'b']) + await UserRoles.addRoleToUser('userId', 'test') + + await session.assertClaims([UserRoles.UserRoleClaim.validators.includes('test')]) + }) + it('should validate permissions', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserRoles.init(), Session.init({ getTokenTransferMethod: () => 'cookie' })], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + await UserRoles.createNewRoleOrAddPermissions('test', ['a', 'b']) + await UserRoles.addRoleToUser('userId', 'test') + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + + await session.assertClaims([UserRoles.PermissionClaim.validators.includes('a')]) + + let err + try { + await session.assertClaims([UserRoles.PermissionClaim.validators.includes('nope')]) + } + catch (ex) { + console.log(ex) + err = ex + } + assert.ok(err) + assert.strictEqual(err.type, 'INVALID_CLAIMS') + assert.strictEqual(err.payload.length, 1) + assert.strictEqual(err.payload[0].id, 'st-perm') + assert.deepStrictEqual(err.payload[0].reason, { + message: 'wrong value', + expectedToInclude: 'nope', + actualValue: ['a', 'b'], + }) + }) + it('should validate permissions after refetching', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [ + UserRoles.init({ + skipAddingPermissionsToAccessToken: true, + }), + Session.init({ getTokenTransferMethod: () => 'cookie' }), + ], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const session = await Session.createNewSession(mockRequest(), mockResponse(), 'userId') + await UserRoles.createNewRoleOrAddPermissions('test', ['a', 'b']) + await UserRoles.addRoleToUser('userId', 'test') + + await session.assertClaims([UserRoles.PermissionClaim.validators.includes('a')]) + }) + }) +}) diff --git a/test/userroles/config.test.js b/test/userroles/config.test.js deleted file mode 100644 index cac2dc39d..000000000 --- a/test/userroles/config.test.js +++ /dev/null @@ -1,46 +0,0 @@ -const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); -const { ProcessState } = require("../../lib/build/processState"); -const STExpress = require("../.."); -const UserRolesRecipe = require("../../lib/build/recipe/userroles/recipe").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`configTest: ${printPath("[test/userroles/config.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("recipe init", () => { - it("should work fine without config", async function () { - await startST(); - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [UserRolesRecipe.init(), SessionRecipe.init()], - }); - - // Only run for version >= 2.14 - const querier = Querier.getNewInstanceOrThrowError(undefined); - const apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - await UserRolesRecipe.getInstanceOrThrowError(); - }); - }); -}); diff --git a/test/userroles/config.test.ts b/test/userroles/config.test.ts new file mode 100644 index 000000000..c1379028d --- /dev/null +++ b/test/userroles/config.test.ts @@ -0,0 +1,46 @@ +import { ProcessState } from 'supertokens-node/processState' +import STExpress from 'supertokens-node' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`configTest: ${printPath('[test/userroles/config.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('recipe init', () => { + it('should work fine without config', async function () { + await startST() + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [UserRolesRecipe.init(), SessionRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + await UserRolesRecipe.getInstanceOrThrowError() + }) + }) +}) diff --git a/test/userroles/createNewRoleOrAddPermissions.test.js b/test/userroles/createNewRoleOrAddPermissions.test.js deleted file mode 100644 index 8aa5b81bd..000000000 --- a/test/userroles/createNewRoleOrAddPermissions.test.js +++ /dev/null @@ -1,229 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`createNewRoleOrAddPermissionsTest: ${printPath( - "[test/userroles/createNewRoleOrAddPermissions.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("createNewRoleOrAddPermissions", () => { - it("create a new role", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const result = await UserRolesRecipe.createNewRoleOrAddPermissions("newRole", []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - }); - - it("create the same role twice", async function () { - await startST(); - - const role = "role"; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []); - assert.strictEqual(result.status, "OK"); - assert(!result.createdNewRole); - } - }); - - it("create a role with permissions", async function () { - await startST(); - - const role = "role"; - const permissions = ["permission1"]; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - { - // get permissions for roles - const result = await UserRolesRecipe.getPermissionsForRole(role); - assert.strictEqual(result.status, "OK"); - assert(areArraysEqual(result.permissions, permissions)); - } - }); - - it("add new permissions to a role", async function () { - await startST(); - - const role = "role"; - const permissions = ["permission1"]; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // add additional permissions to the role - - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, [ - "permission2", - "permission3", - ]); - assert.strictEqual(result.status, "OK"); - assert(!result.createdNewRole); - } - - // check that the permissions have been added - - { - const finalPermissions = ["permission1", "permission2", "permission3"]; - const result = await UserRolesRecipe.getPermissionsForRole(role); - assert.strictEqual(result.status, "OK"); - assert(areArraysEqual(finalPermissions, result.permissions)); - } - }); - - it("add duplicate permission", async function () { - await startST(); - - const role = "role"; - const permissions = ["permission1"]; - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // add duplicate permissions to the role - - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions); - assert.strictEqual(result.status, "OK"); - assert(!result.createdNewRole); - } - - // check that no additional permission has been added - - { - const result = await UserRolesRecipe.getPermissionsForRole(role); - assert.strictEqual(result.status, "OK"); - assert(areArraysEqual(result.permissions, permissions)); - } - }); - }); -}); diff --git a/test/userroles/createNewRoleOrAddPermissions.test.ts b/test/userroles/createNewRoleOrAddPermissions.test.ts new file mode 100644 index 000000000..90c427e5a --- /dev/null +++ b/test/userroles/createNewRoleOrAddPermissions.test.ts @@ -0,0 +1,224 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { areArraysEqual, cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`createNewRoleOrAddPermissionsTest: ${printPath( + '[test/userroles/createNewRoleOrAddPermissions.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('createNewRoleOrAddPermissions', () => { + it('create a new role', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const result = await UserRolesRecipe.createNewRoleOrAddPermissions('newRole', []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + }) + + it('create the same role twice', async function () { + await startST() + + const role = 'role' + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []) + assert.strictEqual(result.status, 'OK') + assert(!result.createdNewRole) + } + }) + + it('create a role with permissions', async function () { + await startST() + + const role = 'role' + const permissions = ['permission1'] + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + { + // get permissions for roles + const result = await UserRolesRecipe.getPermissionsForRole(role) + assert.strictEqual(result.status, 'OK') + assert(areArraysEqual(result.permissions, permissions)) + } + }) + + it('add new permissions to a role', async function () { + await startST() + + const role = 'role' + const permissions = ['permission1'] + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // add additional permissions to the role + + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, [ + 'permission2', + 'permission3', + ]) + assert.strictEqual(result.status, 'OK') + assert(!result.createdNewRole) + } + + // check that the permissions have been added + + { + const finalPermissions = ['permission1', 'permission2', 'permission3'] + const result = await UserRolesRecipe.getPermissionsForRole(role) + assert.strictEqual(result.status, 'OK') + assert(areArraysEqual(finalPermissions, result.permissions)) + } + }) + + it('add duplicate permission', async function () { + await startST() + + const role = 'role' + const permissions = ['permission1'] + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // add duplicate permissions to the role + + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions) + assert.strictEqual(result.status, 'OK') + assert(!result.createdNewRole) + } + + // check that no additional permission has been added + + { + const result = await UserRolesRecipe.getPermissionsForRole(role) + assert.strictEqual(result.status, 'OK') + assert(areArraysEqual(result.permissions, permissions)) + } + }) + }) +}) diff --git a/test/userroles/deleteRole.test.js b/test/userroles/deleteRole.test.js deleted file mode 100644 index ba6451603..000000000 --- a/test/userroles/deleteRole.test.js +++ /dev/null @@ -1,107 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`getPermissionsForRole: ${printPath("[test/userroles/getPermissionsForRole.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("deleteRole", () => { - it("create roles, add them to a user and delete one of the roles", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const roles = ["role1", "role2", "role3"]; - const userId = "user"; - - // create role and it to user - { - for (let role in roles) { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(roles[role], []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - - const response = await UserRolesRecipe.addRoleToUser(userId, roles[role]); - assert.strictEqual(response.status, "OK"); - assert(!response.didUserAlreadyHaveRole); - } - } - - // delete role, check that role does not exist, check that user does not have role - { - const result = await UserRolesRecipe.deleteRole("role3"); - assert.strictEqual(result.status, "OK"); - assert(result.didRoleExist); - - const allRolesResponse = await UserRolesRecipe.getAllRoles(); - assert.strictEqual(allRolesResponse.status, "OK"); - assert.strictEqual(allRolesResponse.roles.length, 2); - assert(!allRolesResponse.roles.includes("role3")); - - const allUserRoles = await UserRolesRecipe.getRolesForUser(userId); - assert.strictEqual(allUserRoles.status, "OK"); - assert.strictEqual(allUserRoles.roles.length, 2); - assert(!allUserRoles.roles.includes("role3")); - } - }); - - it("delete a role that does not exist", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const result = await UserRolesRecipe.deleteRole("unknownRole"); - assert.strictEqual(result.status, "OK"); - assert(!result.didRoleExist); - }); - }); -}); diff --git a/test/userroles/deleteRole.test.ts b/test/userroles/deleteRole.test.ts new file mode 100644 index 000000000..aacd12c72 --- /dev/null +++ b/test/userroles/deleteRole.test.ts @@ -0,0 +1,105 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getPermissionsForRole: ${printPath('[test/userroles/getPermissionsForRole.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('deleteRole', () => { + it('create roles, add them to a user and delete one of the roles', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const roles = ['role1', 'role2', 'role3'] + const userId = 'user' + + // create role and it to user + { + for (const role in roles) { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(roles[role], []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + + const response = await UserRolesRecipe.addRoleToUser(userId, roles[role]) + assert.strictEqual(response.status, 'OK') + assert(!response.didUserAlreadyHaveRole) + } + } + + // delete role, check that role does not exist, check that user does not have role + { + const result = await UserRolesRecipe.deleteRole('role3') + assert.strictEqual(result.status, 'OK') + assert(result.didRoleExist) + + const allRolesResponse = await UserRolesRecipe.getAllRoles() + assert.strictEqual(allRolesResponse.status, 'OK') + assert.strictEqual(allRolesResponse.roles.length, 2) + assert(!allRolesResponse.roles.includes('role3')) + + const allUserRoles = await UserRolesRecipe.getRolesForUser(userId) + assert.strictEqual(allUserRoles.status, 'OK') + assert.strictEqual(allUserRoles.roles.length, 2) + assert(!allUserRoles.roles.includes('role3')) + } + }) + + it('delete a role that does not exist', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const result = await UserRolesRecipe.deleteRole('unknownRole') + assert.strictEqual(result.status, 'OK') + assert(!result.didRoleExist) + }) + }) +}) diff --git a/test/userroles/getPermissionsForRole.test.js b/test/userroles/getPermissionsForRole.test.js deleted file mode 100644 index e6263eb0b..000000000 --- a/test/userroles/getPermissionsForRole.test.js +++ /dev/null @@ -1,90 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`getPermissionsForRole: ${printPath("[test/userroles/getPermissionsForRole.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getPermissionsForRole", () => { - it("get permissions for a role", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const role = "role"; - const permissions = ["permission1", "permission2", "permission3"]; - - // create role - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // retrieve the permissions for the role - const result = await UserRolesRecipe.getPermissionsForRole(role); - - assert.strictEqual(result.status, "OK"); - assert(areArraysEqual(permissions, result.permissions)); - }); - - it("get permissions for an unknown role", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - // retrieve the users for role that does not exist - const result = await UserRolesRecipe.getPermissionsForRole("unknownRole"); - assert.strictEqual(result.status, "UNKNOWN_ROLE_ERROR"); - }); - }); -}); diff --git a/test/userroles/getPermissionsForRole.test.ts b/test/userroles/getPermissionsForRole.test.ts new file mode 100644 index 000000000..618eef077 --- /dev/null +++ b/test/userroles/getPermissionsForRole.test.ts @@ -0,0 +1,88 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { areArraysEqual, cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getPermissionsForRole: ${printPath('[test/userroles/getPermissionsForRole.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getPermissionsForRole', () => { + it('get permissions for a role', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const role = 'role' + const permissions = ['permission1', 'permission2', 'permission3'] + + // create role + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // retrieve the permissions for the role + const result = await UserRolesRecipe.getPermissionsForRole(role) + + assert.strictEqual(result.status, 'OK') + assert(areArraysEqual(permissions, result.permissions)) + }) + + it('get permissions for an unknown role', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + // retrieve the users for role that does not exist + const result = await UserRolesRecipe.getPermissionsForRole('unknownRole') + assert.strictEqual(result.status, 'UNKNOWN_ROLE_ERROR') + }) + }) +}) diff --git a/test/userroles/getRolesForUser.test.js b/test/userroles/getRolesForUser.test.js deleted file mode 100644 index 2a1701454..000000000 --- a/test/userroles/getRolesForUser.test.js +++ /dev/null @@ -1,67 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`getRolesForUser: ${printPath("[test/userroles/getRolesForUser.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getRolesForUser", () => { - it("create roles, add them to a user check that the user has the roles", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const userId = "userId"; - const roles = ["role1", "role2", "role3"]; - - // create roles and add them to a user - - for (let role in roles) { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(roles[role], []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - - const response = await UserRolesRecipe.addRoleToUser(userId, roles[role]); - assert.strictEqual(response.status, "OK"); - assert(!response.didUserAlreadyHaveRole); - } - - // check that user has the roles - const result = await UserRolesRecipe.getRolesForUser(userId); - assert.strictEqual(result.status, "OK"); - assert(areArraysEqual(roles, result.roles)); - }); - }); -}); diff --git a/test/userroles/getRolesForUser.test.ts b/test/userroles/getRolesForUser.test.ts new file mode 100644 index 000000000..92369649b --- /dev/null +++ b/test/userroles/getRolesForUser.test.ts @@ -0,0 +1,66 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { ProcessState } from 'supertokens-node/processState' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { areArraysEqual, cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getRolesForUser: ${printPath('[test/userroles/getRolesForUser.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getRolesForUser', () => { + it('create roles, add them to a user check that the user has the roles', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const userId = 'userId' + const roles = ['role1', 'role2', 'role3'] + + // create roles and add them to a user + + for (const role in roles) { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(roles[role], []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + + const response = await UserRolesRecipe.addRoleToUser(userId, roles[role]) + assert.strictEqual(response.status, 'OK') + assert(!response.didUserAlreadyHaveRole) + } + + // check that user has the roles + const result = await UserRolesRecipe.getRolesForUser(userId) + assert.strictEqual(result.status, 'OK') + assert(areArraysEqual(roles, result.roles)) + }) + }) +}) diff --git a/test/userroles/getRolesThatHavePermissions.test.js b/test/userroles/getRolesThatHavePermissions.test.js deleted file mode 100644 index eb91c771c..000000000 --- a/test/userroles/getRolesThatHavePermissions.test.js +++ /dev/null @@ -1,96 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`getRolesThatHavePermissions: ${printPath( - "[test/userroles/getRolesThatHavePermissions.test.js]" -)}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getRolesThatHavePermissions", () => { - it("get roles that have permissions", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const roles = ["role1", "role2", "role3"]; - const permission = "permission"; - - // create roles with permission - { - for (let role in roles) { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(roles[role], [permission]); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - } - - // retrieve roles with permission - { - const result = await UserRolesRecipe.getRolesThatHavePermission(permission); - assert.strictEqual(result.status, "OK"); - assert(areArraysEqual(roles, result.roles)); - } - }); - - it("get roles for unknown permission", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - // retrieve roles for unknown permission - const result = await UserRolesRecipe.getRolesThatHavePermission("unknownPermission"); - assert.strictEqual(result.status, "OK"); - assert.strictEqual(result.roles.length, 0); - }); - }); -}); diff --git a/test/userroles/getRolesThatHavePermissions.test.ts b/test/userroles/getRolesThatHavePermissions.test.ts new file mode 100644 index 000000000..91df57d43 --- /dev/null +++ b/test/userroles/getRolesThatHavePermissions.test.ts @@ -0,0 +1,94 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { areArraysEqual, cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getRolesThatHavePermissions: ${printPath( + '[test/userroles/getRolesThatHavePermissions.test.ts]', +)}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getRolesThatHavePermissions', () => { + it('get roles that have permissions', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const roles = ['role1', 'role2', 'role3'] + const permission = 'permission' + + // create roles with permission + { + for (const role in roles) { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(roles[role], [permission]) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + } + + // retrieve roles with permission + { + const result = await UserRolesRecipe.getRolesThatHavePermission(permission) + assert.strictEqual(result.status, 'OK') + assert(areArraysEqual(roles, result.roles)) + } + }) + + it('get roles for unknown permission', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + // retrieve roles for unknown permission + const result = await UserRolesRecipe.getRolesThatHavePermission('unknownPermission') + assert.strictEqual(result.status, 'OK') + assert.strictEqual(result.roles.length, 0) + }) + }) +}) diff --git a/test/userroles/getUsersThatHaveRole.test.js b/test/userroles/getUsersThatHaveRole.test.js deleted file mode 100644 index 67bb59166..000000000 --- a/test/userroles/getUsersThatHaveRole.test.js +++ /dev/null @@ -1,96 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`getUsersThatHaveRole: ${printPath("[test/userroles/getUsersThatHaveRole.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getUsersThatHaveRole", () => { - it("get users for a role", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const users = ["user1", "user2", "user3"]; - const role = "role"; - - // create role - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // add them to a user - for (let user in users) { - const response = await UserRolesRecipe.addRoleToUser(users[user], role); - assert.strictEqual(response.status, "OK"); - assert(!response.didUserAlreadyHaveRole); - } - - // retrieve the users for role - const result = await UserRolesRecipe.getUsersThatHaveRole(role); - assert.strictEqual(result.status, "OK"); - assert(areArraysEqual(users, result.users)); - }); - - it("get users for an unknown role", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - // retrieve the users for role which that not exist - const result = await UserRolesRecipe.getUsersThatHaveRole("unknownRole"); - assert.strictEqual(result.status, "UNKNOWN_ROLE_ERROR"); - }); - }); -}); diff --git a/test/userroles/getUsersThatHaveRole.test.ts b/test/userroles/getUsersThatHaveRole.test.ts new file mode 100644 index 000000000..a88528960 --- /dev/null +++ b/test/userroles/getUsersThatHaveRole.test.ts @@ -0,0 +1,94 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { areArraysEqual, cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getUsersThatHaveRole: ${printPath('[test/userroles/getUsersThatHaveRole.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getUsersThatHaveRole', () => { + it('get users for a role', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const users = ['user1', 'user2', 'user3'] + const role = 'role' + + // create role + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // add them to a user + for (const user in users) { + const response = await UserRolesRecipe.addRoleToUser(users[user], role) + assert.strictEqual(response.status, 'OK') + assert(!response.didUserAlreadyHaveRole) + } + + // retrieve the users for role + const result = await UserRolesRecipe.getUsersThatHaveRole(role) + assert.strictEqual(result.status, 'OK') + assert(areArraysEqual(users, result.users)) + }) + + it('get users for an unknown role', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + // retrieve the users for role which that not exist + const result = await UserRolesRecipe.getUsersThatHaveRole('unknownRole') + assert.strictEqual(result.status, 'UNKNOWN_ROLE_ERROR') + }) + }) +}) diff --git a/test/userroles/removePermissionsFromRole.test.js b/test/userroles/removePermissionsFromRole.test.js deleted file mode 100644 index a1b19dc4f..000000000 --- a/test/userroles/removePermissionsFromRole.test.js +++ /dev/null @@ -1,98 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`getPermissionsForRole: ${printPath("[test/userroles/getPermissionsForRole.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("getPermissionsForRole", () => { - it("remove permissions from a role", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const role = "role"; - const permissions = ["permission1", "permission2", "permission3"]; - - // create role with permissions - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // remove permissions from role - { - const result = await UserRolesRecipe.removePermissionsFromRole(role, ["permission3"]); - assert.strictEqual(result.status, "OK"); - } - - // check that permission has been removed from the role - { - const result = await UserRolesRecipe.getPermissionsForRole(role); - assert.strictEqual(result.status, "OK"); - assert.strictEqual(result.permissions.length, 2); - assert(!result.permissions.includes("permission3")); - } - }); - - it("remove permissions from an unknown role", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - // remove permission from an unknown role - const result = await UserRolesRecipe.removePermissionsFromRole("unknownRole"); - assert.strictEqual(result.status, "UNKNOWN_ROLE_ERROR"); - }); - }); -}); diff --git a/test/userroles/removePermissionsFromRole.test.ts b/test/userroles/removePermissionsFromRole.test.ts new file mode 100644 index 000000000..8825cbb5e --- /dev/null +++ b/test/userroles/removePermissionsFromRole.test.ts @@ -0,0 +1,96 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`getPermissionsForRole: ${printPath('[test/userroles/getPermissionsForRole.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('getPermissionsForRole', () => { + it('remove permissions from a role', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const role = 'role' + const permissions = ['permission1', 'permission2', 'permission3'] + + // create role with permissions + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, permissions) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // remove permissions from role + { + const result = await UserRolesRecipe.removePermissionsFromRole(role, ['permission3']) + assert.strictEqual(result.status, 'OK') + } + + // check that permission has been removed from the role + { + const result = await UserRolesRecipe.getPermissionsForRole(role) + assert.strictEqual(result.status, 'OK') + assert.strictEqual(result.permissions.length, 2) + assert(!result.permissions.includes('permission3')) + } + }) + + it('remove permissions from an unknown role', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + // remove permission from an unknown role + const result = await UserRolesRecipe.removePermissionsFromRole('unknownRole') + assert.strictEqual(result.status, 'UNKNOWN_ROLE_ERROR') + }) + }) +}) diff --git a/test/userroles/removeUserRole.test.js b/test/userroles/removeUserRole.test.js deleted file mode 100644 index f1fd35d90..000000000 --- a/test/userroles/removeUserRole.test.js +++ /dev/null @@ -1,156 +0,0 @@ -const assert = require("assert"); - -const { printPath, setupST, startST, killAllST, cleanST, areArraysEqual } = require("../utils"); -const STExpress = require("../.."); -const { ProcessState } = require("../../lib/build/processState"); -const UserRolesRecipe = require("../../lib/build/recipe/userroles").default; -const { Querier } = require("../../lib/build/querier"); -const { maxVersion } = require("../../lib/build/utils"); -const { default: SessionRecipe } = require("../../lib/build/recipe/session/recipe"); - -describe(`removeUserRoleTest: ${printPath("[test/userroles/removeUserRole.test.js]")}`, function () { - beforeEach(async function () { - await killAllST(); - await setupST(); - ProcessState.getInstance().reset(); - }); - - after(async function () { - await killAllST(); - await cleanST(); - }); - - describe("removeUserRole", () => { - it("remove role from user", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const userId = "userId"; - const role = "role"; - - // create a new role - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // add the role to a user - { - const result = await UserRolesRecipe.addRoleToUser(userId, role); - assert.strictEqual(result.status, "OK"); - assert(!result.didUserAlreadyHaveRole); - } - - // check that user has role - { - const result = await UserRolesRecipe.getRolesForUser(userId); - assert.strictEqual(result.status, "OK"); - assert.strictEqual(result.roles.length, 1); - assert.strictEqual(result.roles[0], role); - } - - // remove role from user - { - const result = await UserRolesRecipe.removeUserRole(userId, role); - assert.strictEqual(result.status, "OK"); - assert(result.didUserHaveRole); - } - - // check that the user does not have the role - { - const result = await UserRolesRecipe.getRolesForUser(userId); - assert.strictEqual(result.status, "OK"); - assert.strictEqual(result.roles.length, 0); - } - }); - - it("remove a role the user does not have", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const userId = "userId"; - const role = "role"; - - // create a new role - { - const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []); - assert.strictEqual(result.status, "OK"); - assert(result.createdNewRole); - } - - // remove role from user - { - const result = await UserRolesRecipe.removeUserRole(userId, role); - assert.strictEqual(result.status, "OK"); - assert(!result.didUserHaveRole); - } - }); - - it("remove an unknown role from the user", async function () { - await startST(); - - STExpress.init({ - supertokens: { - connectionURI: "http://localhost:8080", - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - websiteDomain: "supertokens.io", - }, - recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], - }); - - // Only run for version >= 2.14 - let querier = Querier.getNewInstanceOrThrowError(undefined); - let apiVersion = await querier.getAPIVersion(); - if (maxVersion(apiVersion, "2.13") === "2.13") { - return this.skip(); - } - - const userId = "userId"; - const role = "unknownRole"; - - // remove an unknown role from user - const result = await UserRolesRecipe.removeUserRole(userId, role); - assert.strictEqual(result.status, "UNKNOWN_ROLE_ERROR"); - }); - }); -}); diff --git a/test/userroles/removeUserRole.test.ts b/test/userroles/removeUserRole.test.ts new file mode 100644 index 000000000..f437f5ae1 --- /dev/null +++ b/test/userroles/removeUserRole.test.ts @@ -0,0 +1,153 @@ +import assert from 'assert' +import STExpress from 'supertokens-node' +import { ProcessState } from 'supertokens-node/processState' +import UserRolesRecipe from 'supertokens-node/recipe/userroles' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import { afterAll, beforeEach, describe, it } from 'vitest' +import { cleanST, killAllST, printPath, setupST, startST } from '../utils' + +describe(`removeUserRoleTest: ${printPath('[test/userroles/removeUserRole.test.ts]')}`, () => { + beforeEach(async () => { + await killAllST() + await setupST() + ProcessState.getInstance().reset() + }) + + afterAll(async () => { + await killAllST() + await cleanST() + }) + + describe('removeUserRole', () => { + it('remove role from user', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const userId = 'userId' + const role = 'role' + + // create a new role + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // add the role to a user + { + const result = await UserRolesRecipe.addRoleToUser(userId, role) + assert.strictEqual(result.status, 'OK') + assert(!result.didUserAlreadyHaveRole) + } + + // check that user has role + { + const result = await UserRolesRecipe.getRolesForUser(userId) + assert.strictEqual(result.status, 'OK') + assert.strictEqual(result.roles.length, 1) + assert.strictEqual(result.roles[0], role) + } + + // remove role from user + { + const result = await UserRolesRecipe.removeUserRole(userId, role) + assert.strictEqual(result.status, 'OK') + assert(result.didUserHaveRole) + } + + // check that the user does not have the role + { + const result = await UserRolesRecipe.getRolesForUser(userId) + assert.strictEqual(result.status, 'OK') + assert.strictEqual(result.roles.length, 0) + } + }) + + it('remove a role the user does not have', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const userId = 'userId' + const role = 'role' + + // create a new role + { + const result = await UserRolesRecipe.createNewRoleOrAddPermissions(role, []) + assert.strictEqual(result.status, 'OK') + assert(result.createdNewRole) + } + + // remove role from user + { + const result = await UserRolesRecipe.removeUserRole(userId, role) + assert.strictEqual(result.status, 'OK') + assert(!result.didUserHaveRole) + } + }) + + it('remove an unknown role from the user', async function () { + await startST() + + STExpress.init({ + supertokens: { + connectionURI: 'http://localhost:8080', + }, + appInfo: { + apiDomain: 'api.supertokens.io', + appName: 'SuperTokens', + websiteDomain: 'supertokens.io', + }, + recipeList: [SessionRecipe.init(), UserRolesRecipe.init()], + }) + + // Only run for version >= 2.14 + const querier = Querier.getNewInstanceOrThrowError(undefined) + const apiVersion = await querier.getAPIVersion() + if (maxVersion(apiVersion, '2.13') === '2.13') + return this.skip() + + const userId = 'userId' + const role = 'unknownRole' + + // remove an unknown role from user + const result = await UserRolesRecipe.removeUserRole(userId, role) + assert.strictEqual(result.status, 'UNKNOWN_ROLE_ERROR') + }) + }) +}) diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index 876bf51e1..000000000 --- a/test/utils.js +++ /dev/null @@ -1,615 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -const { exec } = require("child_process"); -const nock = require("nock"); -const request = require("supertest"); -let fs = require("fs"); -let SuperTokens = require("../lib/build/supertokens").default; -let SessionRecipe = require("../lib/build/recipe/session/recipe").default; -let ThirPartyRecipe = require("../lib/build/recipe/thirdparty/recipe").default; -let ThirPartyPasswordless = require("../lib/build/recipe/thirdpartypasswordless/recipe").default; -let ThirdPartyEmailPasswordRecipe = require("../lib/build/recipe/thirdpartyemailpassword/recipe").default; -let ThirdPartyPasswordlessRecipe = require("../lib/build/recipe/thirdpartypasswordless/recipe").default; -let EmailPasswordRecipe = require("../lib/build/recipe/emailpassword/recipe").default; -let DashboardRecipe = require("../lib/build/recipe/dashboard/recipe").default; -const EmailVerificationRecipe = require("../lib/build/recipe/emailverification/recipe").default; -let JWTRecipe = require("..//lib/build/recipe/jwt/recipe").default; -const UserMetadataRecipe = require("../lib/build/recipe/usermetadata/recipe").default; -let PasswordlessRecipe = require("..//lib/build/recipe/passwordless/recipe").default; -const UserRolesRecipe = require("../lib/build/recipe/userroles/recipe").default; -let { ProcessState } = require("../lib/build/processState"); -let { Querier } = require("../lib/build/querier"); -let { maxVersion } = require("../lib/build/utils"); -const { default: OpenIDRecipe } = require("../lib/build/recipe/openid/recipe"); -const { wrapRequest } = require("../framework/express"); -const { join } = require("path"); - -const users = require("./users.json"); - -module.exports.printPath = function (path) { - return `${createFormat([consoleOptions.yellow, consoleOptions.italic, consoleOptions.dim])}${path}${createFormat([ - consoleOptions.default, - ])}`; -}; - -module.exports.executeCommand = async function (cmd) { - return new Promise((resolve, reject) => { - exec(cmd, (err, stdout, stderr) => { - if (err) { - reject(err); - return; - } - resolve({ stdout, stderr }); - }); - }); -}; - -module.exports.setKeyValueInConfig = async function (key, value) { - return new Promise((resolve, reject) => { - let installationPath = process.env.INSTALL_PATH; - fs.readFile(installationPath + "/config.yaml", "utf8", function (err, data) { - if (err) { - reject(err); - return; - } - let oldStr = new RegExp("((#\\s)?)" + key + "(:|((:\\s).+))\n"); - let newStr = key + ": " + value + "\n"; - let result = data.replace(oldStr, newStr); - fs.writeFile(installationPath + "/config.yaml", result, "utf8", function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - }); -}; - -module.exports.extractInfoFromResponse = function (res) { - let antiCsrf = res.headers["anti-csrf"]; - let accessToken = undefined; - let refreshToken = undefined; - let accessTokenExpiry = undefined; - let refreshTokenExpiry = undefined; - let idRefreshTokenExpiry = undefined; - let accessTokenDomain = undefined; - let refreshTokenDomain = undefined; - let idRefreshTokenDomain = undefined; - let accessTokenHttpOnly = false; - let idRefreshTokenHttpOnly = false; - let refreshTokenHttpOnly = false; - let frontToken = res.headers["front-token"]; - let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; - cookies = cookies === undefined ? [] : cookies; - if (!Array.isArray(cookies)) { - cookies = [cookies]; - } - cookies.forEach((i) => { - if (i.split(";")[0].split("=")[0] === "sAccessToken") { - /** - * if token is sAccessToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0=.eyJzZXNzaW9uSGFuZGxlIjoiMWI4NDBhOTAtMjVmYy00ZjQ4LWE2YWMtMDc0MDIzZjNjZjQwIiwidXNlcklkIjoiIiwicmVmcmVzaFRva2VuSGFzaDEiOiJjYWNhZDNlMGNhMDVkNzRlNWYzNTc4NmFlMGQ2MzJjNDhmMTg1YmZmNmUxNThjN2I2OThkZDYwMzA1NzAyYzI0IiwidXNlckRhdGEiOnt9LCJhbnRpQ3NyZlRva2VuIjoiYTA2MjRjYWItZmIwNy00NTFlLWJmOTYtNWQ3YzU2MjMwZTE4IiwiZXhwaXJ5VGltZSI6MTYyNjUxMjM3NDU4NiwidGltZUNyZWF0ZWQiOjE2MjY1MDg3NzQ1ODYsImxtcnQiOjE2MjY1MDg3NzQ1ODZ9.f1sCkjt0OduS6I6FBQDBLV5zhHXpCU2GXnbe+8OCU6HKG00TX5CM8AyFlOlqzSHABZ7jES/+5k0Ff/rdD34cczlNqICcC4a23AjJg2a097rFrh8/8V7J5fr4UrHLIM4ojZNFz1NyVyDK/ooE6I7soHshEtEVr2XsnJ4q3d+fYs2wwx97PIT82hfHqgbRAzvlv952GYt+OH4bWQE4vTzDqGN7N2OKpn9l2fiCB1Ytzr3ocHRqKuQ8f6xW1n575Q1sSs9F9TtD7lrKfFQH+//6lyKFe2Q1SDc7YU4pE5Cy9Kc/LiqiTU+gsGIJL5qtMzUTG4lX38ugF4QDyNjDBMqCKw==; Max-Age=3599; Expires=Sat, 17 Jul 2021 08:59:34 GMT; Secure; HttpOnly; SameSite=Lax; Path=/' - * i.split(";")[0].split("=")[1] will result in eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0 - */ - accessToken = decodeURIComponent(i.split(";")[0].split("=").slice(1).join("=")); - if (i.split(";")[2].includes("Expires=")) { - accessTokenExpiry = i.split(";")[2].split("=")[1]; - } else if (i.split(";")[2].includes("expires=")) { - accessTokenExpiry = i.split(";")[2].split("=")[1]; - } else { - accessTokenExpiry = i.split(";")[3].split("=")[1]; - } - if (i.split(";")[1].includes("Domain=")) { - accessTokenDomain = i.split(";")[1].split("=")[1]; - } - accessTokenHttpOnly = i.split(";").findIndex((j) => j.includes("HttpOnly")) !== -1; - } else if (i.split(";")[0].split("=")[0] === "sRefreshToken") { - refreshToken = i.split(";")[0].split("=").slice(1).join("="); - if (i.split(";")[2].includes("Expires=")) { - refreshTokenExpiry = i.split(";")[2].split("=")[1]; - } else if (i.split(";")[2].includes("expires=")) { - refreshTokenExpiry = i.split(";")[2].split("=")[1]; - } else { - refreshTokenExpiry = i.split(";")[3].split("=")[1]; - } - if (i.split(";")[1].includes("Domain=")) { - refreshTokenDomain = i.split(";")[1].split("=").slice(1).join("="); - } - refreshTokenHttpOnly = i.split(";").findIndex((j) => j.includes("HttpOnly")) !== -1; - } - }); - - const refreshTokenFromHeader = res.headers["st-refresh-token"]; - const accessTokenFromHeader = res.headers["st-access-token"]; - - const accessTokenFromAny = accessToken === undefined ? accessTokenFromHeader : accessToken; - const refreshTokenFromAny = refreshToken === undefined ? refreshTokenFromHeader : refreshToken; - - return { - status: res.status || res.statusCode, - body: res.body, - antiCsrf, - accessToken, - refreshToken, - accessTokenFromHeader, - refreshTokenFromHeader, - accessTokenFromAny, - refreshTokenFromAny, - accessTokenExpiry, - refreshTokenExpiry, - idRefreshTokenExpiry, - accessTokenDomain, - refreshTokenDomain, - idRefreshTokenDomain, - frontToken, - accessTokenHttpOnly, - refreshTokenHttpOnly, - idRefreshTokenHttpOnly, - }; -}; - -module.exports.extractCookieCountInfo = function (res) { - let accessToken = 0; - let refreshToken = 0; - let idRefreshToken = 0; - let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; - cookies = cookies === undefined ? [] : cookies; - if (!Array.isArray(cookies)) { - cookies = [cookies]; - } - cookies.forEach((i) => { - if (i.split(";")[0].split("=")[0] === "sAccessToken") { - accessToken += 1; - } else if (i.split(";")[0].split("=")[0] === "sRefreshToken") { - refreshToken += 1; - } else { - idRefreshToken += 1; - } - }); - return { - accessToken, - refreshToken, - idRefreshToken, - }; -}; - -module.exports.setupST = async function () { - let installationPath = process.env.INSTALL_PATH; - try { - await module.exports.executeCommand("cd " + installationPath + " && cp temp/licenseKey ./licenseKey"); - } catch (ignore) {} - await module.exports.executeCommand("cd " + installationPath + " && cp temp/config.yaml ./config.yaml"); -}; - -module.exports.cleanST = async function () { - let installationPath = process.env.INSTALL_PATH; - try { - await module.exports.executeCommand("cd " + installationPath + " && rm licenseKey"); - } catch (ignore) {} - await module.exports.executeCommand("cd " + installationPath + " && rm config.yaml"); - await module.exports.executeCommand("cd " + installationPath + " && rm -rf .webserver-temp-*"); - await module.exports.executeCommand("cd " + installationPath + " && rm -rf .started"); -}; - -module.exports.stopST = async function (pid) { - let pidsBefore = await getListOfPids(); - if (pidsBefore.length === 0) { - return; - } - await module.exports.executeCommand("kill " + pid); - let startTime = Date.now(); - while (Date.now() - startTime < 10000) { - let pidsAfter = await getListOfPids(); - if (pidsAfter.includes(pid)) { - await new Promise((r) => setTimeout(r, 100)); - continue; - } else { - return; - } - } - throw new Error("error while stopping ST with PID: " + pid); -}; - -module.exports.resetAll = function () { - SuperTokens.reset(); - SessionRecipe.reset(); - ThirdPartyPasswordlessRecipe.reset(); - ThirdPartyEmailPasswordRecipe.reset(); - ThirPartyPasswordless.reset(); - EmailPasswordRecipe.reset(); - ThirPartyRecipe.reset(); - EmailVerificationRecipe.reset(); - JWTRecipe.reset(); - UserMetadataRecipe.reset(); - UserRolesRecipe.reset(); - PasswordlessRecipe.reset(); - OpenIDRecipe.reset(); - DashboardRecipe.reset(); - ProcessState.getInstance().reset(); -}; - -module.exports.killAllST = async function () { - let pids = await getListOfPids(); - for (let i = 0; i < pids.length; i++) { - await module.exports.stopST(pids[i]); - } - module.exports.resetAll(); - nock.cleanAll(); -}; - -module.exports.killAllSTCoresOnly = async function () { - let pids = await getListOfPids(); - for (let i = 0; i < pids.length; i++) { - await module.exports.stopST(pids[i]); - } -}; - -module.exports.startST = async function (host = "localhost", port = 8080) { - return new Promise(async (resolve, reject) => { - let installationPath = process.env.INSTALL_PATH; - let pidsBefore = await getListOfPids(); - let returned = false; - module.exports - .executeCommand( - "cd " + - installationPath + - ` && java -Djava.security.egd=file:/dev/urandom -classpath "./core/*:./plugin-interface/*" io.supertokens.Main ./ DEV host=` + - host + - " port=" + - port + - " test_mode" - ) - .catch((err) => { - if (!returned) { - returned = true; - reject(err); - } - }); - let startTime = Date.now(); - while (Date.now() - startTime < 30000) { - let pidsAfter = await getListOfPids(); - if (pidsAfter.length <= pidsBefore.length) { - await new Promise((r) => setTimeout(r, 100)); - continue; - } - let nonIntersection = pidsAfter.filter((x) => !pidsBefore.includes(x)); - if (nonIntersection.length !== 1) { - if (!returned) { - returned = true; - reject("something went wrong while starting ST"); - } - } else { - if (!returned) { - returned = true; - resolve(nonIntersection[0]); - } - } - } - if (!returned) { - returned = true; - reject("could not start ST process"); - } - }); -}; - -async function getListOfPids() { - let installationPath = process.env.INSTALL_PATH; - let currList; - try { - currList = (await module.exports.executeCommand("cd " + installationPath + " && ls .started/")).stdout; - } catch (err) { - return []; - } - currList = currList.split("\n"); - let result = []; - for (let i = 0; i < currList.length; i++) { - let item = currList[i]; - if (item === "") { - continue; - } - try { - let pid = (await module.exports.executeCommand("cd " + installationPath + " && cat .started/" + item)) - .stdout; - pid = pid.split("\n")[0]; - result.push(pid); - } catch (err) {} - } - return result; -} - -function createFormat(options) { - if (options.length === 0) { - return ``; - } - let format = `\x1b[`; - for (let i = 0; i < options.length; i++) { - format += options[i]; - if (i !== options.length - 1) { - format += `;`; - } - } - format += `m`; - return format; -} - -const consoleOptions = { - default: 0, - bold: 1, - dim: 2, - italic: 3, - underline: 4, - blink: 5, - white: 29, - black: 30, - red: 31, - green: 32, - yellow: 33, - blue: 34, - purple: 35, - cyan: 36, -}; - -module.exports.signUPRequest = async function (app, email, password) { - return new Promise(function (resolve) { - request(app) - .post("/auth/signup") - .set("st-auth-mode", "cookie") - .send({ - formFields: [ - { - id: "password", - value: password, - }, - { - id: "email", - value: email, - }, - ], - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); -}; - -module.exports.signUPRequestEmptyJSON = async function (app) { - return new Promise(function (resolve) { - request(app) - .post("/auth/signup") - .send({}) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); -}; - -module.exports.signUPRequestNoBody = async function (app) { - return new Promise(function (resolve) { - request(app) - .post("/auth/signup") - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); -}; - -module.exports.signInUPCustomRequest = async function (app, email, id) { - nock("https://test.com").post("/oauth/token").reply(200, { - id, - email, - }); - return new Promise(function (resolve) { - request(app) - .post("/auth/signinup") - .send({ - thirdPartyId: "custom", - code: "abcdefghj", - redirectURI: "http://127.0.0.1/callback", - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); -}; - -module.exports.emailVerifyTokenRequest = async function (app, accessToken, antiCsrf, userId) { - let result = await new Promise(function (resolve) { - request(app) - .post("/auth/user/email/verify/token") - .set("Cookie", ["sAccessToken=" + accessToken]) - .set("anti-csrf", antiCsrf) - .send({ - userId, - }) - .end((err, res) => { - if (err) { - resolve(undefined); - } else { - resolve(res); - } - }); - }); - - // wait for the callback to be called... - await new Promise((res) => setTimeout(res, 500)); - - return result; -}; - -module.exports.mockLambdaProxyEvent = function (path, httpMethod, headers, body, proxy) { - return { - path, - httpMethod, - headers, - body, - requestContext: { - path: `${proxy}${path}`, - }, - }; -}; - -module.exports.mockLambdaProxyEventV2 = function (path, httpMethod, headers, body, proxy, cookies, queryParams) { - return { - version: "2.0", - httpMethod, - headers, - body, - cookies, - requestContext: { - http: { - path: `${proxy}${path}`, - }, - stage: proxy.slice(1), - }, - queryStringParameters: queryParams, - }; -}; - -module.exports.isCDIVersionCompatible = async function (compatibleCDIVersion) { - let currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion(); - - if ( - maxVersion(currCDIVersion, compatibleCDIVersion) === compatibleCDIVersion && - currCDIVersion !== compatibleCDIVersion - ) { - return false; - } - return true; -}; - -module.exports.generateRandomCode = function (size) { - let characters = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; - let randomString = ""; - - //loop to select a new character in each iteration - for (let i = 0; i < size; i++) { - let randdomNumber = Math.floor(Math.random() * characters.length); - randomString += characters.substring(randdomNumber, randdomNumber + 1); - } - return randomString; -}; -module.exports.delay = async function (time) { - await new Promise((r) => setTimeout(r, time * 1000)); -}; - -module.exports.areArraysEqual = function (arr1, arr2) { - if (arr1.length !== arr2.length) { - return false; - } - - arr1.sort(); - arr2.sort(); - - for (let index in arr1) { - if (arr1[index] !== arr2[index]) { - return false; - } - } - - return true; -}; - -/** - * - * @returns {import("express").Response} - */ -module.exports.mockResponse = () => { - const headers = {}; - const res = { - getHeaders: () => headers, - getHeader: (key) => headers[key], - setHeader: (key, val) => (headers[key] = val), - }; - return res; -}; - -/** - * - * @returns {import("express").Request} - */ -module.exports.mockRequest = () => { - const headers = {}; - const req = { - headers, - get: (key) => headers[key], - header: (key) => headers[key], - }; - return req; -}; - -module.exports.getAllFilesInDirectory = (path) => { - return fs - .readdirSync(path, { - withFileTypes: true, - }) - .flatMap((file) => { - if (file.isDirectory()) { - return this.getAllFilesInDirectory(join(path, file.name)); - } else { - return join(path, file.name); - } - }); -}; - -module.exports.createUsers = async (emailpassword = null, passwordless = null, thirdparty = null) => { - const usersArray = users.users; - for (let i = 0; i < usersArray.length; i++) { - const user = usersArray[i]; - if (user.recipe === "emailpassword" && emailpassword !== null) { - await emailpassword.signUp(user.email, user.password); - } - if (user.recipe === "passwordless" && passwordless !== null) { - if (user.email !== undefined) { - const codeResponse = await passwordless.createCode({ - email: user.email, - }); - await passwordless.consumeCode({ - preAuthSessionId: codeResponse.preAuthSessionId, - deviceId: codeResponse.deviceId, - userInputCode: codeResponse.userInputCode, - }); - } else { - const codeResponse = await passwordless.createCode({ - phoneNumber: user.phone, - }); - await passwordless.consumeCode({ - preAuthSessionId: codeResponse.preAuthSessionId, - deviceId: codeResponse.deviceId, - userInputCode: codeResponse.userInputCode, - }); - } - } - - if (user.recipe === "thirdparty" && thirdparty !== null) { - await thirdparty.signInUp(user.provider, user.userId, user.email); - } - } -}; diff --git a/test/utils.test.js b/test/utils.test.js deleted file mode 100644 index 0d84bc480..000000000 --- a/test/utils.test.js +++ /dev/null @@ -1,20 +0,0 @@ -const assert = require("assert"); -const { getFromObjectCaseInsensitive } = require("../lib/build/utils"); - -describe("SuperTokens utils test", () => { - it("Test getFromObjectCaseInsensitive", () => { - const testObj = { - AuthOriZation: "test", - }; - - assert.equal(getFromObjectCaseInsensitive("test", testObj), undefined); - // Exact - assert.equal(getFromObjectCaseInsensitive("AuthOriZation", testObj), "test"); - // All lower case - assert.equal(getFromObjectCaseInsensitive("authorization", testObj), "test"); - // Traditional case - assert.equal(getFromObjectCaseInsensitive("Authorization", testObj), "test"); - // Weird casing - assert.equal(getFromObjectCaseInsensitive("authoriZation", testObj), "test"); - }); -}); diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 000000000..7c7116706 --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,21 @@ +import assert from 'assert' +import { getFromObjectCaseInsensitive } from 'supertokens-node/utils' +import { describe, it } from 'vitest' + +describe('SuperTokens utils test', () => { + it('Test getFromObjectCaseInsensitive', () => { + const testObj = { + AuthOriZation: 'test', + } + + assert.equal(getFromObjectCaseInsensitive('test', testObj), undefined) + // Exact + assert.equal(getFromObjectCaseInsensitive('AuthOriZation', testObj), 'test') + // All lower case + assert.equal(getFromObjectCaseInsensitive('authorization', testObj), 'test') + // Traditional case + assert.equal(getFromObjectCaseInsensitive('Authorization', testObj), 'test') + // Weird casing + assert.equal(getFromObjectCaseInsensitive('authoriZation', testObj), 'test') + }) +}) diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 000000000..12a5fefbf --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,620 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { exec } from 'node:child_process' +import fs from 'fs' +import { join } from 'node:path' +import nock from 'nock' +import request from 'supertest' +import SuperTokens from 'supertokens-node/supertokens' +import SessionRecipe from 'supertokens-node/recipe/session/recipe' +import ThirdPartyRecipe from 'supertokens-node/recipe/thirdparty/recipe' +import ThirdPartyPasswordlessRecipe from 'supertokens-node/recipe/thirdpartypasswordless/recipe' +import ThirdPartyEmailPasswordRecipe from 'supertokens-node/recipe/thirdpartyemailpassword/recipe' +import EmailPasswordRecipe from 'supertokens-node/recipe/emailpassword/recipe' +import DashboardRecipe from 'supertokens-node/recipe/dashboard/recipe' +import EmailVerificationRecipe from 'supertokens-node/recipe/emailverification/recipe' +import JWTRecipe from 'supertokens-node/recipe/jwt/recipe' +import UserMetadataRecipe from 'supertokens-node/recipe/usermetadata/recipe' +import PasswordlessRecipe from 'supertokens-node/recipe/passwordless/recipe' +import UserRolesRecipe from 'supertokens-node/recipe/userroles/recipe' +import { ProcessState } from 'supertokens-node/processState' +import { Querier } from 'supertokens-node/querier' +import { maxVersion } from 'supertokens-node/utils' +import { OpenIdRecipe } from 'supertokens-node/recipe/openid/recipe' + +import users from './users.json' + + +export async function executeCommand(cmd: string): Promise<{ stdout: string; stderr: string }> { + const cwd = process.cwd() + return new Promise((resolve, reject) => { + exec(cmd, + { cwd }, + (err, stdout, stderr) => { + if (err) { + reject(err) + return + } + resolve({ stdout, stderr }) + }) + }) +} + +export async function setKeyValueInConfig(key: any, value: any) { + return new Promise((resolve, reject) => { + const installationPath = process.env.INSTALL_PATH + fs.readFile(`${installationPath}/config.yaml`, 'utf8', (err, data) => { + if (err) { + reject(err) + return + } + const oldStr = new RegExp(`((#\\s)?)${key}(:|((:\\s).+))\n`) + const newStr = `${key}: ${value}\n` + const result = data.replace(oldStr, newStr) + fs.writeFile(`${installationPath}/config.yaml`, result, 'utf8', (err) => { + if (err) + reject(err) + + else + resolve('done') + }) + }) + }) +} + +export function extractInfoFromResponse(res: any) { + /* eslint-disable prefer-const */ + let antiCsrf = res.headers['anti-csrf'] + let accessToken + let refreshToken + let accessTokenExpiry + let refreshTokenExpiry + let idRefreshTokenExpiry + let accessTokenDomain + let refreshTokenDomain + let idRefreshTokenDomain + let accessTokenHttpOnly = false + let idRefreshTokenHttpOnly = false + let refreshTokenHttpOnly = false + let frontToken = res.headers['front-token'] + let cookies = res.headers['set-cookie'] || res.headers['Set-Cookie'] + cookies = cookies === undefined ? [] : cookies + if (!Array.isArray(cookies)) + cookies = [cookies] + + cookies.forEach((i: any) => { + if (i.split(';')[0].split('=')[0] === 'sAccessToken') { + /** + * if token is sAccessToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0=.eyJzZXNzaW9uSGFuZGxlIjoiMWI4NDBhOTAtMjVmYy00ZjQ4LWE2YWMtMDc0MDIzZjNjZjQwIiwidXNlcklkIjoiIiwicmVmcmVzaFRva2VuSGFzaDEiOiJjYWNhZDNlMGNhMDVkNzRlNWYzNTc4NmFlMGQ2MzJjNDhmMTg1YmZmNmUxNThjN2I2OThkZDYwMzA1NzAyYzI0IiwidXNlckRhdGEiOnt9LCJhbnRpQ3NyZlRva2VuIjoiYTA2MjRjYWItZmIwNy00NTFlLWJmOTYtNWQ3YzU2MjMwZTE4IiwiZXhwaXJ5VGltZSI6MTYyNjUxMjM3NDU4NiwidGltZUNyZWF0ZWQiOjE2MjY1MDg3NzQ1ODYsImxtcnQiOjE2MjY1MDg3NzQ1ODZ9.f1sCkjt0OduS6I6FBQDBLV5zhHXpCU2GXnbe+8OCU6HKG00TX5CM8AyFlOlqzSHABZ7jES/+5k0Ff/rdD34cczlNqICcC4a23AjJg2a097rFrh8/8V7J5fr4UrHLIM4ojZNFz1NyVyDK/ooE6I7soHshEtEVr2XsnJ4q3d+fYs2wwx97PIT82hfHqgbRAzvlv952GYt+OH4bWQE4vTzDqGN7N2OKpn9l2fiCB1Ytzr3ocHRqKuQ8f6xW1n575Q1sSs9F9TtD7lrKfFQH+//6lyKFe2Q1SDc7YU4pE5Cy9Kc/LiqiTU+gsGIJL5qtMzUTG4lX38ugF4QDyNjDBMqCKw==; Max-Age=3599; Expires=Sat, 17 Jul 2021 08:59:34 GMT; Secure; HttpOnly; SameSite=Lax; Path=/' + * i.split(";")[0].split("=")[1] will result in eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0 + */ + accessToken = decodeURIComponent(i.split(';')[0].split('=').slice(1).join('=')) + if (i.split(';')[2].includes('Expires=')) + accessTokenExpiry = i.split(';')[2].split('=')[1] + + else if (i.split(';')[2].includes('expires=')) + accessTokenExpiry = i.split(';')[2].split('=')[1] + + else + accessTokenExpiry = i.split(';')[3].split('=')[1] + + if (i.split(';')[1].includes('Domain=')) + accessTokenDomain = i.split(';')[1].split('=')[1] + + accessTokenHttpOnly = i.split(';').findIndex((j: any) => j.includes('HttpOnly')) !== -1 + } + else if (i.split(';')[0].split('=')[0] === 'sRefreshToken') { + refreshToken = i.split(';')[0].split('=').slice(1).join('=') + if (i.split(';')[2].includes('Expires=')) + refreshTokenExpiry = i.split(';')[2].split('=')[1] + + else if (i.split(';')[2].includes('expires=')) + refreshTokenExpiry = i.split(';')[2].split('=')[1] + + else + refreshTokenExpiry = i.split(';')[3].split('=')[1] + + if (i.split(';')[1].includes('Domain=')) + refreshTokenDomain = i.split(';')[1].split('=').slice(1).join('=') + + refreshTokenHttpOnly = i.split(';').findIndex((j: any) => j.includes('HttpOnly')) !== -1 + } + }) + + const refreshTokenFromHeader = res.headers['st-refresh-token'] + const accessTokenFromHeader = res.headers['st-access-token'] + + const accessTokenFromAny = accessToken === undefined ? accessTokenFromHeader : accessToken + const refreshTokenFromAny = refreshToken === undefined ? refreshTokenFromHeader : refreshToken + + return { + status: res.status || res.statusCode, + body: res.body, + antiCsrf, + accessToken, + refreshToken, + accessTokenFromHeader, + refreshTokenFromHeader, + accessTokenFromAny, + refreshTokenFromAny, + accessTokenExpiry, + refreshTokenExpiry, + idRefreshTokenExpiry, + accessTokenDomain, + refreshTokenDomain, + idRefreshTokenDomain, + frontToken, + accessTokenHttpOnly, + refreshTokenHttpOnly, + idRefreshTokenHttpOnly, + } +} + +export function extractCookieCountInfo(res: { headers: { [x: string]: any } }) { + let accessToken = 0 + let refreshToken = 0 + let idRefreshToken = 0 + let cookies = res.headers['set-cookie'] || res.headers['Set-Cookie'] + cookies = cookies === undefined ? [] : cookies + if (!Array.isArray(cookies)) + cookies = [cookies] + + cookies.forEach((i: string) => { + if (i.split(';')[0].split('=')[0] === 'sAccessToken') + accessToken += 1 + + else if (i.split(';')[0].split('=')[0] === 'sRefreshToken') + refreshToken += 1 + + else + idRefreshToken += 1 + }) + return { + accessToken, + refreshToken, + idRefreshToken, + } +} + +export async function setupST() { + const installationPath = process.env.INSTALL_PATH + try { + await executeCommand(`cd ${installationPath} && cp temp/licenseKey ./licenseKey`) + } + catch (ignore) {} + await executeCommand(`cd ${installationPath} && cp temp/config.yaml ./config.yaml`) +} + +export async function cleanST() { + const installationPath = process.env.INSTALL_PATH + try { + await executeCommand(`cd ${installationPath} && rm licenseKey`) + } + catch (ignore) {} + await executeCommand(`cd ${installationPath} && rm config.yaml`) + await executeCommand(`cd ${installationPath} && rm -rf .webserver-temp-*`) + await executeCommand(`cd ${installationPath} && rm -rf .started`) +} + +export async function stopST(pid: any) { + const pidsBefore = await getListOfPids() + if (pidsBefore.length === 0) + return + + await executeCommand(`kill ${pid}`) + const startTime = Date.now() + while (Date.now() - startTime < 10000) { + const pidsAfter = await getListOfPids() + if (pidsAfter.includes(pid)) { + await new Promise(resolve => setTimeout(resolve, 100)) + continue + } + else { + return + } + } + throw new Error(`error while stopping ST with PID: ${pid}`) +} + +export function resetAll() { + SuperTokens.reset() + SessionRecipe.reset() + ThirdPartyPasswordlessRecipe.reset() + ThirdPartyEmailPasswordRecipe.reset() + ThirdPartyPasswordlessRecipe.reset() + EmailPasswordRecipe.reset() + ThirdPartyRecipe.reset() + EmailVerificationRecipe.reset() + JWTRecipe.reset() + UserMetadataRecipe.reset() + UserRolesRecipe.reset() + PasswordlessRecipe.reset() + OpenIdRecipe.reset() + DashboardRecipe.reset() + ProcessState.getInstance().reset() +} + +export async function killAllST() { + const pids = await getListOfPids() + for (let i = 0; i < pids.length; i++) + await stopST(pids[i]) + + resetAll() + nock.cleanAll() +} + +export async function killAllSTCoresOnly() { + const pids = await getListOfPids() + for (let i = 0; i < pids.length; i++) + await stopST(pids[i]) +} + +export async function startST(host = 'localhost', port = 8080) { + // TODO: remove this async + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + const installationPath = process.env.INSTALL_PATH + const pidsBefore = await getListOfPids() + let returned = false + executeCommand(`cd ${installationPath} && java -Djava.security.egd=file:/dev/urandom -classpath "./core/*:./plugin-interface/*" io.supertokens.Main ./ DEV host=${host} port=${port} test_mode`).catch((err: any) => { + if (!returned) { + returned = true + reject(err) + } + }) + const startTime = Date.now() + while (Date.now() - startTime < 30000) { + const pidsAfter = await getListOfPids() + if (pidsAfter.length <= pidsBefore.length) { + await new Promise(resolve => setTimeout(resolve, 100)) + continue + } + const nonIntersection = pidsAfter.filter(x => !pidsBefore.includes(x)) + if (nonIntersection.length !== 1) { + if (!returned) { + returned = true + reject(new Error('something went wrong while starting ST')) + } + } + else { + if (!returned) { + returned = true + resolve(nonIntersection[0]) + } + } + } + if (!returned) { + returned = true + reject(new Error('could not start ST process')) + } + }) +} + +async function getListOfPids() { + const installationPath = process.env.INSTALL_PATH + let currList: string | any[] + try { + currList = (await executeCommand(`cd ${installationPath} && ls .started/`)).stdout + } + catch (err) { + return [] + } + + currList = currList.split('\n') + + const result = [] + for (let i = 0; i < currList.length; i++) { + const item = currList[i] + if (item === '') + continue + + try { + let pid = (await executeCommand(`cd ${installationPath} && cat .started/${item}`)).stdout + pid = pid.split('\n')[0] + result.push(pid) + } + catch (err) {} + } + return result +} + +function createFormat(options: string | any[]) { + if (options.length === 0) + return '' + + let format = '\x1B[' + for (let i = 0; i < options.length; i++) { + format += options[i] + if (i !== options.length - 1) + format += ';' + } + format += 'm' + return format +} + +const consoleOptions = { + default: 0, + bold: 1, + dim: 2, + italic: 3, + underline: 4, + blink: 5, + white: 29, + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + purple: 35, + cyan: 36, +} + +export async function signUPRequest(app: any, email: any, password: any) { + return new Promise((resolve) => { + request(app) + .post('/auth/signup') + .set('st-auth-mode', 'cookie') + .send({ + formFields: [ + { + id: 'password', + value: password, + }, + { + id: 'email', + value: email, + }, + ], + }) + .end((err: any, res: unknown) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) +} + +export async function signUPRequestEmptyJSON(app: any) { + return new Promise((resolve) => { + request(app) + .post('/auth/signup') + .send({}) + .end((err: any, res: unknown) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) +} + +export async function signUPRequestNoBody(app: any) { + return new Promise((resolve) => { + request(app) + .post('/auth/signup') + .end((err: any, res: unknown) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) +} + +export async function signInUPCustomRequest(app: any, email: any, id: any) { + nock('https://test.com').post('/oauth/token').reply(200, { + id, + email, + }) + return new Promise((resolve) => { + request(app) + .post('/auth/signinup') + .send({ + thirdPartyId: 'custom', + code: 'abcdefghj', + redirectURI: 'http://127.0.0.1/callback', + }) + .end((err: any, res: unknown) => { + if (err) + resolve(undefined) + + else + resolve(res) + }) + }) +} + +export async function emailVerifyTokenRequest(app: any, accessToken: any, antiCsrf: any, userId: any) { + const result = await new Promise((resolve) => { + request(app) + .post('/auth/user/email/verify/token') + .set('Cookie', [`sAccessToken=${accessToken}`]) + .set('anti-csrf', antiCsrf) + .send({ + userId, + }) + .end((err: any, res: unknown) => { + if (err) + resolve(undefined) + else + resolve(res) + }) + }) + + // wait for the callback to be called... + await new Promise(resolve => setTimeout(resolve, 500)) + + return result +} + +export function mockLambdaProxyEvent(path: any, httpMethod: any, headers: any, body: any, proxy: any) { + return { + path, + httpMethod, + headers, + body, + requestContext: { + path: `${proxy}${path}`, + }, + } +} + +export function mockLambdaProxyEventV2(path: any, httpMethod: any, headers: any, body: any, proxy: string | any[], cookies: any, queryParams: any) { + return { + version: '2.0', + httpMethod, + headers, + body, + cookies, + requestContext: { + http: { + path: `${proxy}${path}`, + }, + stage: proxy.slice(1), + }, + queryStringParameters: queryParams, + } +} + +export async function isCDIVersionCompatible(compatibleCDIVersion: any) { + const currCDIVersion = await Querier.getNewInstanceOrThrowError(undefined).getAPIVersion() + + if ( + maxVersion(currCDIVersion, compatibleCDIVersion) === compatibleCDIVersion + && currCDIVersion !== compatibleCDIVersion + ) + return false + + return true +} + +export function generateRandomCode(size: number) { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz' + let randomString = '' + + // loop to select a new character in each iteration + for (let i = 0; i < size; i++) { + const randdomNumber = Math.floor(Math.random() * characters.length) + randomString += characters.substring(randdomNumber, randdomNumber + 1) + } + return randomString +} +export async function delay(time: number) { + await new Promise(resolve => setTimeout(resolve, time * 1000)) +} + +export function areArraysEqual(arr1: any[], arr2: any[]) { + if (arr1.length !== arr2.length) + return false + + arr1.sort() + arr2.sort() + + for (const index in arr1) { + if (arr1[index] !== arr2[index]) + return false + } + + return true +} + +/** + * + * @returns {import("express").Response} + */ +export const mockResponse = () => { + const headers = {} as any + const res = { + getHeaders: () => headers, + getHeader: (key: string | number) => headers[key], + setHeader: (key: string | number, val: any) => (headers[key] = val), + } + return res +} + +/** + * + * @returns {import("express").Request} + */ +export const mockRequest = () => { + const headers = {} as any + const req = { + headers, + get: (key: string | number) => headers[key], + header: (key: string | number) => headers[key], + } + return req +} + +export function printPath(path: any) { + return `${createFormat([consoleOptions.yellow, consoleOptions.italic, consoleOptions.dim])}${path}${createFormat([ + consoleOptions.default, + ])}` +} + +export const getAllFilesInDirectory = (path: any): any => { + return fs + .readdirSync(path, { + withFileTypes: true, + }) + .flatMap((file: any) => { + if (file.isDirectory()) + return getAllFilesInDirectory(join(path, file.name)) + + else + return join(path, file.name) + }) +} + + +export const createUsers = async (emailpassword: any, passwordless: any, thirdparty: any) => { + const usersArray = users.users; + for (let i = 0; i < usersArray.length; i++) { + const user = usersArray[i]; + if (user.recipe === "emailpassword" && emailpassword !== null) { + await emailpassword.signUp(user.email, user.password); + } + if (user.recipe === "passwordless" && passwordless !== null) { + if (user.email !== undefined) { + const codeResponse = await passwordless.createCode({ + email: user.email, + }); + await passwordless.consumeCode({ + preAuthSessionId: codeResponse.preAuthSessionId, + deviceId: codeResponse.deviceId, + userInputCode: codeResponse.userInputCode, + }); + } else { + const codeResponse = await passwordless.createCode({ + phoneNumber: user.phone, + }); + await passwordless.consumeCode({ + preAuthSessionId: codeResponse.preAuthSessionId, + deviceId: codeResponse.deviceId, + userInputCode: codeResponse.userInputCode, + }); + } + } + + if (user.recipe === "thirdparty" && thirdparty !== null) { + await thirdparty.signInUp(user.provider, user.userId, user.email); + } + } +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..51d7eb5e9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 14", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "lib": [ + "es2020" + ], + "module": "commonjs", + "target": "es2020", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + + "allowSyntheticDefaultImports": true, + "importHelpers": true, + "types": [ + "vite/client", + "node" + ], + "paths": { + "overrideableBuilder": [ + "./src/overrideableBuilder/index.ts" + ], + "supertokens-node/*": [ + "./src/*" + ], + "supertokens-node": [ + "./src/index.ts" + ] + } + }, + "include": [ + "src/**/*", + "test/**/*" + ] +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 000000000..8c9cded69 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,55 @@ +import type { Options } from 'tsup' + +import pkg from './package.json' +const external = [ + ...Object.keys(pkg.dependencies || {}), +] + +export default { + entryPoints: [ + 'src/index.ts', + 'src/nextjs.ts', + 'src/framework/**/index.ts', + 'src/types.ts', + + 'src/recipe/dashboard/index.ts', + + 'src/recipe/emailpassword/index.ts', + 'src/recipe/emailpassword/emaildelivery/index.ts', + 'src/recipe/emailverification/index.ts', + 'src/recipe/emailverification/emaildelivery/index.ts', + + 'src/recipe/jwt/index.ts', + 'src/recipe/openid/index.ts', + + 'src/recipe/passwordless/index.ts', + 'src/recipe/passwordless/emaildelivery/index.ts', + 'src/recipe/passwordless/smsdelivery/index.ts', + + 'src/recipe/session/framework/**', + 'src/recipe/session/claims.ts', + 'src/recipe/session/index.ts', + + 'src/recipe/thirdparty/index.ts', + 'src/recipe/thirdparty/providers/**', + + 'src/recipe/thirdpartyemailpassword/index.ts', + 'src/recipe/thirdpartyemailpassword/emaildelivery/index.ts', + + 'src/recipe/thirdpartypasswordless/index.ts', + 'src/recipe/thirdpartypasswordless/emaildelivery/index.ts', + 'src/recipe/thirdpartypasswordless/smsdelivery/index.ts', + + 'src/recipe/usermetadata/index.ts', + 'src/recipe/userroles/index.ts', + + 'src/processState.ts', + 'src/utils.ts', + ], + outDir: 'dist', + format: ['esm', 'cjs'], + clean: true, + dts: true, + minify: true, + external, +} diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 909977c27..000000000 --- a/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "../lib/build/types"; -/** - * 'export *' does not re-export a default. - * import NextJS from "supertokens-node/nextjs"; - * the above import statement won't be possible unless either - * - user add "esModuleInterop": true in their tsconfig.json file - * - we do the following change: - */ -import * as _default from "../lib/build/types"; -export default _default; diff --git a/types/index.js b/types/index.js deleted file mode 100644 index 37012d0d4..000000000 --- a/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("../lib/build/types")); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 000000000..fe172b637 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,31 @@ +import path from 'node:path' +import { defineConfig } from 'vitest/config' + +const alias = (p: string) => path.resolve(__dirname, p) + +export default defineConfig({ + test: { + coverage: { + provider: 'c8', // or 'c8', + reporter: ['text', 'json-summary', 'json', 'html'], + }, + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/docs/**', + 'test/utils.ts', + ], + include: [ + './test/**/*.test.ts', + ], + singleThread: true, + hookTimeout: 61000, + testTimeout: 190000, + }, + resolve: { + alias: { + 'supertokens-node': alias('./src/'), + 'overrideableBuilder': alias('./src/overrideableBuilder/'), + }, + }, +})